├── .eslintrc.js ├── .github └── workflows │ ├── front.yml │ ├── ios.yml │ ├── server.yml │ └── web.yml ├── .gitignore ├── .prettierrc ├── README.md ├── backend ├── ecosystem.config.js ├── ormconfig.js ├── package-lock.json ├── package.json ├── src │ ├── app.ts │ ├── entities │ │ ├── Album.ts │ │ ├── Artist.ts │ │ ├── Genre.ts │ │ ├── MP3.ts │ │ ├── Mag.ts │ │ ├── Membership.ts │ │ ├── Playlist.ts │ │ ├── Subscribe.ts │ │ ├── Track.ts │ │ └── User.ts │ ├── middlewares │ │ └── auth.ts │ ├── models │ │ └── Log.ts │ ├── passport │ │ ├── index.ts │ │ └── naver.ts │ ├── route │ │ ├── album │ │ │ ├── controller.ts │ │ │ └── index.ts │ │ ├── artist │ │ │ ├── controller.ts │ │ │ └── index.ts │ │ ├── auth │ │ │ ├── controller.ts │ │ │ └── index.ts │ │ ├── auth2 │ │ │ ├── controller.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── library │ │ │ ├── album │ │ │ │ ├── controller.ts │ │ │ │ └── index.ts │ │ │ ├── artist │ │ │ │ ├── controller.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── playlist │ │ │ │ ├── controller.ts │ │ │ │ └── index.ts │ │ │ └── track │ │ │ │ ├── controller.ts │ │ │ │ └── index.ts │ │ ├── log │ │ │ ├── controller.ts │ │ │ └── index.ts │ │ ├── mag │ │ │ ├── controller.ts │ │ │ └── index.ts │ │ ├── playlist │ │ │ ├── controller.ts │ │ │ └── index.ts │ │ ├── sample │ │ │ ├── controller.ts │ │ │ └── index.ts │ │ ├── track │ │ │ ├── controller.ts │ │ │ └── index.ts │ │ └── users │ │ │ ├── controller.ts │ │ │ └── index.ts │ └── services │ │ ├── album │ │ └── index.ts │ │ ├── artist │ │ └── index.ts │ │ ├── auth │ │ └── index.ts │ │ ├── library │ │ ├── album │ │ │ └── index.ts │ │ ├── artist │ │ │ └── index.ts │ │ ├── playlist │ │ │ └── index.ts │ │ └── track │ │ │ └── index.ts │ │ ├── mag │ │ └── index.ts │ │ ├── playlist │ │ └── index.ts │ │ └── track │ │ └── index.ts └── tsconfig.json ├── frontend ├── .babelrc ├── .env.development ├── .env.production ├── README.md ├── images.d.ts ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── album │ │ └── [id].tsx │ ├── artist │ │ └── [id].tsx │ ├── index.tsx │ ├── library │ │ ├── albums │ │ │ └── index.tsx │ │ ├── artists │ │ │ └── index.tsx │ │ ├── playlists │ │ │ └── index.tsx │ │ └── tracks │ │ │ └── index.tsx │ ├── magazines │ │ └── [id].tsx │ ├── playlist │ │ └── [id].tsx │ ├── today.tsx │ └── track │ │ └── [id].tsx ├── public │ └── images │ │ ├── banner-ad-img.png │ │ ├── empty-music-img.png │ │ ├── header-logo.png │ │ ├── modal-img.png │ │ ├── spinnerSmallCat.gif │ │ └── track-hover-img.png ├── src │ ├── api │ │ └── index.ts │ ├── components │ │ ├── AlbumList │ │ │ └── index.tsx │ │ ├── ArtistList │ │ │ └── index.tsx │ │ ├── Common │ │ │ ├── A │ │ │ │ └── index.tsx │ │ │ ├── BoxItem │ │ │ │ └── index.tsx │ │ │ ├── Button │ │ │ │ ├── BoxPlayButton.tsx │ │ │ │ ├── CircleHeartButton.tsx │ │ │ │ └── LargeButton.tsx │ │ │ ├── Card │ │ │ │ ├── AlbumCard │ │ │ │ │ └── index.tsx │ │ │ │ ├── ArtistCard │ │ │ │ │ └── index.tsx │ │ │ │ ├── MagCard │ │ │ │ │ └── index.tsx │ │ │ │ ├── MagLargeCard │ │ │ │ │ └── index.tsx │ │ │ │ └── PlaylistCard │ │ │ │ │ └── index.tsx │ │ │ ├── Carousel │ │ │ │ └── index.tsx │ │ │ ├── CircleImage │ │ │ │ └── index.tsx │ │ │ ├── Dropdown │ │ │ │ ├── ArtistDropdown.tsx │ │ │ │ ├── AuthDropdown.tsx │ │ │ │ └── BoxDropdown.tsx │ │ │ ├── MagTag │ │ │ │ └── index.tsx │ │ │ ├── MagTopItem │ │ │ │ └── index.tsx │ │ │ ├── Modal │ │ │ │ └── index.tsx │ │ │ ├── PlayTrackItem │ │ │ │ └── index.tsx │ │ │ ├── SampleSection │ │ │ │ ├── RelatedArtist.tsx │ │ │ │ └── RelatedPlaylist.tsx │ │ │ ├── Section │ │ │ │ └── index.tsx │ │ │ ├── Spinner │ │ │ │ └── index.tsx │ │ │ └── TrackItem │ │ │ │ └── index.tsx │ │ ├── EventWrapper │ │ │ ├── ClickEventWrapper.tsx │ │ │ └── LibraryEventWrapper.tsx │ │ ├── Layout │ │ │ ├── Footer │ │ │ │ └── index.tsx │ │ │ ├── PlayBar │ │ │ │ └── index.tsx │ │ │ ├── SideBar │ │ │ │ ├── Header │ │ │ │ │ └── index.tsx │ │ │ │ ├── NavBar │ │ │ │ │ ├── NavList │ │ │ │ │ │ └── index.tsx │ │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ ├── MagList │ │ │ └── index.tsx │ │ ├── PlaylistList │ │ │ └── index.tsx │ │ ├── Template │ │ │ ├── Content │ │ │ │ └── index.tsx │ │ │ ├── Detail │ │ │ │ └── index.tsx │ │ │ └── Library │ │ │ │ └── index.tsx │ │ ├── TrackList │ │ │ └── index.tsx │ │ ├── sample-rx.tsx │ │ └── sample.tsx │ ├── constants │ │ ├── description.ts │ │ ├── dropdownText.ts │ │ └── lyricSample.ts │ ├── context │ │ ├── AuthContext.tsx │ │ └── PlayContext.tsx │ ├── hooks │ │ ├── useFetch.ts │ │ └── useLogError.ts │ ├── pages │ │ ├── Detail │ │ │ ├── Album │ │ │ │ └── index.tsx │ │ │ ├── Artist │ │ │ │ └── index.tsx │ │ │ ├── Magazine │ │ │ │ └── index.tsx │ │ │ ├── Playlist │ │ │ │ └── index.tsx │ │ │ └── Track │ │ │ │ └── index.tsx │ │ ├── Library │ │ │ ├── MyAlbum │ │ │ │ └── index.tsx │ │ │ ├── MyArtist │ │ │ │ └── index.tsx │ │ │ ├── MyPlaylist │ │ │ │ └── index.tsx │ │ │ └── MyTrack │ │ │ │ └── index.tsx │ │ └── Today │ │ │ └── index.tsx │ ├── styles │ │ ├── global-styles.ts │ │ ├── themed-components.ts │ │ ├── themes.ts │ │ └── withSizes.ts │ └── utils │ │ ├── getMultipleNames.ts │ │ ├── getRandomUserId.tsx │ │ ├── getRefererFromHeader.ts │ │ ├── getTokenFromCookie.ts │ │ ├── logEventHandler.tsx │ │ ├── trimContentLength.tsx │ │ └── unifyMetaData.ts └── tsconfig.json ├── iOS ├── MiniVIBE │ ├── .swiftlint.yml │ ├── MiniVIBE.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── MiniVIBE.xcscheme │ ├── MiniVIBE.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── MiniVIBE │ │ ├── Injected │ │ │ └── DependencyInjector.swift │ │ ├── Models │ │ │ ├── Contents │ │ │ │ ├── Album.swift │ │ │ │ ├── Artist.swift │ │ │ │ ├── Magazine.swift │ │ │ │ ├── Playlist.swift │ │ │ │ ├── Song.swift │ │ │ │ └── Video.swift │ │ │ ├── Enviroments │ │ │ │ └── MusicPlayer.swift │ │ │ └── Events │ │ │ │ ├── CoreDataEvent.swift │ │ │ │ ├── Date+customDateFormat.swift │ │ │ │ ├── ErrorEvent.swift │ │ │ │ ├── Event+Encodable.swift │ │ │ │ ├── EventName.swift │ │ │ │ ├── LibraryEvent.swift │ │ │ │ ├── LoginEvent.swift │ │ │ │ ├── MoveEvent.swift │ │ │ │ ├── MusicEvent.swift │ │ │ │ ├── String+EventParameters.swift │ │ │ │ └── TapEvent.swift │ │ ├── Persistence │ │ │ ├── MiniVIBE.xcdatamodeld │ │ │ │ ├── .xccurrentversion │ │ │ │ ├── MiniVIBE v2.xcdatamodel │ │ │ │ │ └── contents │ │ │ │ ├── MiniVIBE v3.xcdatamodel │ │ │ │ │ └── contents │ │ │ │ └── MiniVIBE.xcdatamodel │ │ │ │ │ └── contents │ │ │ └── Persistence.swift │ │ ├── Preview Content │ │ │ └── Preview Assets.xcassets │ │ │ │ └── Contents.json │ │ ├── Repositories │ │ │ ├── LocalRepository.swift │ │ │ ├── RealServerRepository.swift │ │ │ └── ServerRepository.swift │ │ ├── Resources │ │ │ ├── Assets.xcassets │ │ │ │ ├── AppIcon.appiconset │ │ │ │ │ ├── 1024.png │ │ │ │ │ ├── 114.png │ │ │ │ │ ├── 120-1.png │ │ │ │ │ ├── 120.png │ │ │ │ │ ├── 180.png │ │ │ │ │ ├── 29.png │ │ │ │ │ ├── 40.png │ │ │ │ │ ├── 57.png │ │ │ │ │ ├── 58.png │ │ │ │ │ ├── 60.png │ │ │ │ │ ├── 80.png │ │ │ │ │ ├── 87.png │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── Icon-152.png │ │ │ │ │ ├── Icon-167.png │ │ │ │ │ ├── Icon-20.png │ │ │ │ │ ├── Icon-29.png │ │ │ │ │ ├── Icon-40.png │ │ │ │ │ ├── Icon-41.png │ │ │ │ │ ├── Icon-58.png │ │ │ │ │ ├── Icon-76.png │ │ │ │ │ └── Icon-80.png │ │ │ │ ├── Contents.json │ │ │ │ ├── HomeView │ │ │ │ │ ├── ArtistSection │ │ │ │ │ │ ├── Artists.imageset │ │ │ │ │ │ │ ├── Artists.png │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ └── Contents.json │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── HomeDJStationSection │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ ├── DJStationDetail1.imageset │ │ │ │ │ │ │ ├── 5100133.jpg │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ ├── DJStationDetail2.imageset │ │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ │ └── VIBE_공통_잠못드는밤.png │ │ │ │ │ │ ├── DJStationDetail3.imageset │ │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ │ └── VIBE_공통_캐롤베스트.png │ │ │ │ │ │ ├── HomeDJStationSection1.imageset │ │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ │ └── mood_1_NowHot.png │ │ │ │ │ │ ├── HomeDJStationSection2.imageset │ │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ │ └── mood_2_Hip.png │ │ │ │ │ │ ├── HomeDJStationSection3.imageset │ │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ │ └── mood_3_Happy.png │ │ │ │ │ │ ├── HomeDJStationSection4.imageset │ │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ │ └── mood_4_Sad.png │ │ │ │ │ │ ├── HomeDJStationSection5.imageset │ │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ │ └── mood_5_Love.png │ │ │ │ │ │ ├── HomeDJStationSection6.imageset │ │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ │ └── mood_6_Broken.png │ │ │ │ │ │ └── mixTape.imageset │ │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ │ └── Image.png │ │ │ │ │ ├── HomeMainSection │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ ├── HomeMainSection1.imageset │ │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ │ └── KakaoTalk_Photo_2020-11-23-16-16-51.jpeg │ │ │ │ │ │ ├── HomeMainSection2.imageset │ │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ │ └── KakaoTalk_Photo_2020-11-23-16-24-18-1.jpeg │ │ │ │ │ │ └── HomeMainSection3.imageset │ │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ │ └── KakaoTalk_Photo_2020-11-23-16-24-18-2.jpeg │ │ │ │ │ ├── HomePlayListSection │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ ├── HomePlayListSection1.imageset │ │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ │ └── KakaoTalk_Photo_2020-11-25-01-54-33.jpeg │ │ │ │ │ │ ├── HomePlayListSection2.imageset │ │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ │ └── HomePlayListSection2.png │ │ │ │ │ │ └── HomePlayListSection3.imageset │ │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ │ └── HomePlayListSection3.png │ │ │ │ │ ├── Magazine │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ ├── mag-dummy1.imageset │ │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ │ └── mag-dummy1.png │ │ │ │ │ │ ├── mag-dummy2.imageset │ │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ │ └── mag-dummy2.png │ │ │ │ │ │ └── mag-dummy3.imageset │ │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ │ └── mag-dummy3.png │ │ │ │ │ ├── NewAlbum │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ ├── newAlbum-dummy1.imageset │ │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ │ └── newAlbum-dummy1.jpg │ │ │ │ │ │ ├── newAlbum-dummy2.imageset │ │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ │ └── newAlbum-dummy2.jpg │ │ │ │ │ │ └── newAlbum-dummy3.imageset │ │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ │ └── newAlbum-dummy3.jpg │ │ │ │ │ ├── Now │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ ├── home-now.imageset │ │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ │ └── home-now.png │ │ │ │ │ │ ├── now-dummy1.imageset │ │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ │ └── now-dummy1.jpeg │ │ │ │ │ │ ├── now-dummy2.imageset │ │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ │ └── now-dummy2.png │ │ │ │ │ │ └── now-dummy3.imageset │ │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ │ └── now-dummy3.png │ │ │ │ │ ├── VibeRecommend │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ ├── vibe-dummy1.imageset │ │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ │ └── vibe-dummy1.png │ │ │ │ │ │ ├── vibe-dummy2.imageset │ │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ │ └── vibe-dummy2.png │ │ │ │ │ │ └── vibe-dummy3.imageset │ │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ │ └── vibe-dummy3.png │ │ │ │ │ └── login.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ ├── profileImage(1)-1.jpg │ │ │ │ │ │ ├── profileImage(1)-2.jpg │ │ │ │ │ │ └── profileImage(1).jpg │ │ │ │ ├── VIBEBackground.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── VIBEPink.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── VIBETitle.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── splash.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── splash-1.png │ │ │ │ │ ├── splash-2.png │ │ │ │ │ └── splash.png │ │ │ │ └── splashBackground.colorset │ │ │ │ │ └── Contents.json │ │ │ └── Info.plist │ │ ├── Services │ │ │ ├── EventRequest.swift │ │ │ └── EventService.swift │ │ ├── System │ │ │ └── MiniVIBEApp.swift │ │ ├── UI │ │ │ ├── ChartView │ │ │ │ ├── ChartView.swift │ │ │ │ └── MusicVideoTop50SectionView.swift │ │ │ ├── Components │ │ │ │ ├── AlbumSection │ │ │ │ │ ├── AlbumDetailView.swift │ │ │ │ │ ├── AlbumMoreView.swift │ │ │ │ │ ├── AlbumSectionView.swift │ │ │ │ │ └── AlbumSectionViewModel.swift │ │ │ │ ├── Common │ │ │ │ │ ├── AsyncImageView.swift │ │ │ │ │ ├── DetailHeaderView.swift │ │ │ │ │ ├── ImageItemView.swift │ │ │ │ │ ├── MoreHeaderView.swift │ │ │ │ │ ├── PlayShuffleHeaderButton.swift │ │ │ │ │ ├── RankChangeView.swift │ │ │ │ │ └── SectionScrollView.swift │ │ │ │ ├── FiveRowSongGridSection │ │ │ │ │ ├── FiveRowSongGridViewModel.swift │ │ │ │ │ ├── FiveRowSongGridMoreView.swift │ │ │ │ │ └── FiveRowSongGridView.swift │ │ │ │ └── PlayListSection │ │ │ │ │ ├── PlaylistDetailView.swift │ │ │ │ │ ├── PlaylistMoreView.swift │ │ │ │ │ ├── PlaylistSectionView.swift │ │ │ │ │ └── PlaylistSectionViewModel.swift │ │ │ ├── ContentView.swift │ │ │ ├── Extension │ │ │ │ ├── CGFloat+ImageWidths.swift │ │ │ │ ├── CGFloat+defaultValues.swift │ │ │ │ ├── Color+vibeColors.swift │ │ │ │ ├── View+name.swift │ │ │ │ ├── View+vibeMainText.swift │ │ │ │ └── View+vibeTitles.swift │ │ │ ├── LibraryView │ │ │ │ ├── LibraryAlbumView.swift │ │ │ │ ├── LibraryArtistView.swift │ │ │ │ ├── LibraryPlaylistView.swift │ │ │ │ ├── LibrarySongView.swift │ │ │ │ └── LibraryView.swift │ │ │ ├── PlayingBar │ │ │ │ ├── Blur.swift │ │ │ │ ├── MusicPlayerView.swift │ │ │ │ └── NowPlayingBarView.swift │ │ │ ├── SearchVIew │ │ │ │ ├── NewsItemView.swift │ │ │ │ └── SearchView.swift │ │ │ ├── TodayView │ │ │ │ ├── ArtistSection │ │ │ │ │ └── ArtistSection.swift │ │ │ │ ├── DJStationSection │ │ │ │ │ ├── DJStationItem.swift │ │ │ │ │ ├── DJStationMoreView │ │ │ │ │ │ └── DJStationMoreView.swift │ │ │ │ │ └── DJStationSectionView.swift │ │ │ │ ├── MagazineSection │ │ │ │ │ └── MagazineSectionView.swift │ │ │ │ ├── NowSection │ │ │ │ │ ├── NowItemView.swift │ │ │ │ │ ├── NowReplayItem.swift │ │ │ │ │ └── NowSectionView.swift │ │ │ │ ├── SummarySection │ │ │ │ │ ├── HomeSummarySectionItemView.swift │ │ │ │ │ ├── SummaryItem.swift │ │ │ │ │ └── SummarySectionView.swift │ │ │ │ ├── TodayFooterView.swift │ │ │ │ ├── TodayHeaderView.swift │ │ │ │ └── TodayView.swift │ │ │ ├── VideoView │ │ │ │ ├── GenreSectionView.swift │ │ │ │ ├── MockServerView │ │ │ │ │ └── MockServerView.swift │ │ │ │ ├── VideoHeaderView.swift │ │ │ │ └── VideoView.swift │ │ │ └── WebView.swift │ │ └── Utilities │ │ │ ├── AsyncImageHelper │ │ │ ├── AsyncImageCache.swift │ │ │ └── AsyncImageNetwork.swift │ │ │ ├── ItemRequest.swift │ │ │ ├── ItemResponse.swift │ │ │ ├── KeyChain.swift │ │ │ ├── MockItemFactory.swift │ │ │ ├── Network.swift │ │ │ ├── Networking.swift │ │ │ ├── Reachability.swift │ │ │ └── RequestProviding.swift │ ├── MiniVIBETests │ │ ├── EventServiceTests.swift │ │ ├── Extension │ │ │ ├── CoreData+Equatable.swift │ │ │ ├── Event+Equatable.swift │ │ │ ├── ItemResponse+Encodable.swift │ │ │ ├── Magazine+Equatable.swift │ │ │ └── NetworkError+Equatable.swift │ │ ├── Info.plist │ │ ├── LocalRepositoryTests.swift │ │ ├── Mock │ │ │ ├── FailPersistenceController.swift │ │ │ ├── MockRequest.swift │ │ │ └── TestPersistenceController.swift │ │ ├── NetworkTests.swift │ │ ├── PersistenceControllerTests.swift │ │ └── ServerRepositoryTests.swift │ ├── Podfile │ └── Podfile.lock └── README.md ├── package-lock.json ├── package.json └── settings.json /.github/workflows/front.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | name: Web Front CI 3 | # Controls when the action will run. Triggers the workflow on push or pull request 4 | # events but only for the main branch 5 | on: 6 | push: 7 | branches: [frontend] 8 | pull_request: 9 | branches: [frontend] 10 | jobs: 11 | build: 12 | name: Front Build and Deploy 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: executing remote ssh commands using private key 16 | uses: appleboy/ssh-action@master 17 | with: 18 | host: ${{ secrets.HOST }} 19 | username: ${{ secrets.USERNAME }} 20 | port: ${{ secrets.PORT }} 21 | key: ${{ secrets.PRIVATEKEY }} 22 | script: | 23 | cd Project01-C-User-Event-Collector 24 | git checkout frontend 25 | git pull origin frontend 26 | npm install 27 | cd frontend 28 | npm install 29 | npm run build --if-present -------------------------------------------------------------------------------- /.github/workflows/server.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | name: server-deploy 3 | # Controls when the action will run. Triggers the workflow on push or pull request 4 | # events but only for the main branch 5 | on: 6 | push: 7 | branches: [backend] 8 | pull_request: 9 | branches: [backend] 10 | jobs: 11 | build: 12 | name: API Server Build and Deploy 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: executing remote ssh commands using private key 16 | uses: appleboy/ssh-action@master 17 | with: 18 | host: ${{ secrets.HOST }} 19 | username: ${{ secrets.USERNAME }} 20 | port: ${{ secrets.PORT }} 21 | key: ${{ secrets.PRIVATEKEY }} 22 | script: | 23 | cd Project01-C-User-Event-Collector 24 | git checkout backend 25 | git pull origin backend 26 | npm install 27 | cd backend 28 | npm install 29 | npm run start 30 | whoami -------------------------------------------------------------------------------- /.github/workflows/web.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | name: WEB CI 3 | # Controls when the action will run. Triggers the workflow on push or pull request 4 | # events but only for the main branch 5 | on: 6 | push: 7 | branches: [web] 8 | pull_request: 9 | branches: [web] 10 | jobs: 11 | build: 12 | name: API Server Build and Deploy 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: executing remote ssh commands using private key 16 | uses: appleboy/ssh-action@master 17 | with: 18 | host: ${{ secrets.HOST }} 19 | username: ${{ secrets.USERNAME }} 20 | port: ${{ secrets.PORT }} 21 | key: ${{ secrets.PRIVATEKEY }} 22 | script: | 23 | cd Project01-C-User-Event-Collector 24 | git checkout web 25 | git pull origin web 26 | npm install 27 | cd backend 28 | npm install 29 | npm run start 30 | cd ../frontend 31 | npm install 32 | npm run build --if-present 33 | pm2 start npm --name "next" -- start 34 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "singleQuote": true, 5 | "trailingComma": "all", 6 | "bracketSpacing": true, 7 | "semi": true, 8 | "useTabs": false, 9 | "arrowParens": "avoid", 10 | "endOfLine": "lf" 11 | } -------------------------------------------------------------------------------- /backend/ecosystem.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps: [ 3 | { 4 | name: 'vibe-api-server', 5 | script: './dist/app.js', 6 | instances: 0, 7 | exec_mode: 'cluster', 8 | }, 9 | ], 10 | }; 11 | -------------------------------------------------------------------------------- /backend/ormconfig.js: -------------------------------------------------------------------------------- 1 | const devConfig = { 2 | type: process.env.DB_TYPE, 3 | host: process.env.DB_HOST, 4 | port: process.env.DB_PORT, 5 | username: process.env.DB_USERNAME, 6 | password: process.env.DB_PASSWORD, 7 | database: process.env.DB_DBNAME, 8 | synchronize: true, 9 | logging: false, 10 | entities: ['src/entities/**/*.ts'], 11 | migrations: ['src/migration/**/*.ts'], 12 | subscribers: ['src/subscriber/**/*.ts'], 13 | cli: { 14 | entitiesDir: 'src/entities', 15 | migrationsDir: 'src/migrations', 16 | subscribersDir: 'src/subscriber', 17 | }, 18 | }; 19 | 20 | const prodConfig = { 21 | type: process.env.DB_TYPE, 22 | host: process.env.DB_HOST, 23 | port: process.env.DB_PORT, 24 | username: process.env.DB_USERNAME, 25 | password: process.env.DB_PASSWORD, 26 | database: process.env.DB_DBNAME, 27 | synchronize: true, 28 | logging: false, 29 | entities: ['dist/entities/**/*.js'], 30 | migrations: ['dist/migration/**/*.js'], 31 | subscribers: ['dist/subscriber/**/*.js'], 32 | cli: { 33 | entitiesDir: 'dist/entities', 34 | migrationsDir: 'dist/migrations', 35 | subscribersDir: 'dist/subscriber', 36 | }, 37 | }; 38 | 39 | module.exports = process.env.NODE_ENV === 'production' ? prodConfig : devConfig; 40 | -------------------------------------------------------------------------------- /backend/src/entities/Album.ts: -------------------------------------------------------------------------------- 1 | import { Entity, BaseEntity, PrimaryGeneratedColumn, Column, OneToMany, ManyToMany } from 'typeorm'; 2 | import Artist from './Artist'; 3 | import User from './User'; 4 | import Genre from './Genre'; 5 | import Track from './Track'; 6 | 7 | @Entity() 8 | export default class Album extends BaseEntity { 9 | @PrimaryGeneratedColumn() 10 | id!: number; 11 | 12 | @Column() 13 | name!: string; 14 | 15 | @Column() 16 | imgUrl!: string; 17 | 18 | @Column() 19 | date!: Date; 20 | 21 | @OneToMany(() => Track, track => track.album) 22 | tracks!: Track[]; 23 | 24 | @ManyToMany(() => User, user => user.albums) 25 | users!: User[]; 26 | 27 | @ManyToMany(() => Artist, artist => artist.albums) 28 | artists!: Artist[]; 29 | 30 | @ManyToMany(() => Genre, genre => genre.albums) 31 | genres!: Genre[]; 32 | 33 | static findByUserId(id: number) { 34 | return this.createQueryBuilder('album') 35 | .innerJoin('album.users', 'user') 36 | .leftJoinAndSelect('album.artists', 'artist') 37 | .leftJoinAndSelect('album.tracks', 'track') 38 | .where('user.id = :id', { id }) 39 | .getMany(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /backend/src/entities/Artist.ts: -------------------------------------------------------------------------------- 1 | import { Entity, BaseEntity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from 'typeorm'; 2 | import User from './User'; 3 | import Album from './Album'; 4 | import Track from './Track'; 5 | import Genre from './Genre'; 6 | 7 | export type UserGenderType = 'F' | 'M' | 'U'; 8 | 9 | @Entity() 10 | export default class Artist extends BaseEntity { 11 | @PrimaryGeneratedColumn() 12 | id!: number; 13 | 14 | @Column() 15 | name!: string; 16 | 17 | @Column() 18 | debut!: Date; 19 | 20 | @Column() 21 | imgUrl!: string; 22 | 23 | @ManyToMany(() => User, user => user.artists, { onDelete: 'CASCADE' }) 24 | users!: User[]; 25 | 26 | @ManyToMany(() => Album, album => album.artists, { onDelete: 'CASCADE' }) 27 | @JoinTable({ name: 'ArtistAlbum' }) 28 | albums!: Album[]; 29 | 30 | @ManyToMany(() => Track, track => track.artists, { onDelete: 'CASCADE' }) 31 | @JoinTable({ name: 'ArtistTrack' }) 32 | tracks!: Track[]; 33 | 34 | @ManyToMany(() => Genre, genre => genre.artists, { onDelete: 'CASCADE' }) 35 | @JoinTable({ name: 'ArtistGenre' }) 36 | genres!: Genre[]; 37 | 38 | static findByUserId(id: number) { 39 | return this.createQueryBuilder('artist') 40 | .innerJoin('artist.users', 'user') 41 | .where('user.id = :id', { id }) 42 | .getMany(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /backend/src/entities/Genre.ts: -------------------------------------------------------------------------------- 1 | import { Entity, BaseEntity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from 'typeorm'; 2 | import Artist from './Artist'; 3 | import Album from './Album'; 4 | 5 | @Entity() 6 | export default class Genre extends BaseEntity { 7 | @PrimaryGeneratedColumn() 8 | id!: number; 9 | 10 | @Column() 11 | name!: string; 12 | 13 | @ManyToMany(() => Artist, artist => artist.genres) 14 | artists!: Artist[]; 15 | 16 | @ManyToMany(() => Album, album => album.genres) 17 | @JoinTable({ name: 'AlbumGenre' }) 18 | albums!: Artist[]; 19 | } 20 | -------------------------------------------------------------------------------- /backend/src/entities/MP3.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Column, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; 2 | import User from './User'; 3 | import Track from './Track'; 4 | 5 | @Entity() 6 | export default class MP3 { 7 | @PrimaryGeneratedColumn() 8 | id!: number; 9 | 10 | @Column() 11 | purchaseDate!: Date; 12 | 13 | @ManyToOne(() => User, user => user.mp3) 14 | user!: User; 15 | 16 | @ManyToOne(() => Track, track => track.mp3) 17 | track!: Track; 18 | } 19 | -------------------------------------------------------------------------------- /backend/src/entities/Mag.ts: -------------------------------------------------------------------------------- 1 | import { Entity, BaseEntity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm'; 2 | import Track from './Track'; 3 | 4 | @Entity() 5 | export default class Mag extends BaseEntity { 6 | @PrimaryGeneratedColumn() 7 | id!: number; 8 | 9 | @Column() 10 | title!: string; 11 | 12 | @Column() 13 | imgUrl!: string; 14 | 15 | @Column() 16 | date!: string; 17 | 18 | @Column() 19 | tag!: string; 20 | 21 | @Column() 22 | content!: string; 23 | 24 | @OneToMany(() => Track, track => track.mag) 25 | tracks!: Track[]; 26 | } 27 | -------------------------------------------------------------------------------- /backend/src/entities/Membership.ts: -------------------------------------------------------------------------------- 1 | import { Entity, BaseEntity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm'; 2 | import Subscribe from './Subscribe'; 3 | 4 | @Entity() 5 | export default class Membership extends BaseEntity { 6 | @PrimaryGeneratedColumn() 7 | id!: number; 8 | 9 | @Column() 10 | name!: string; 11 | 12 | @Column() 13 | price!: number; 14 | 15 | @Column() 16 | period!: string; 17 | 18 | @Column() 19 | streaming!: boolean; 20 | 21 | @Column() 22 | offlineStreaming!: boolean; 23 | 24 | @OneToMany(() => Subscribe, subscribe => subscribe.membership, { onDelete: 'CASCADE' }) 25 | subscribe!: Subscribe[]; 26 | } 27 | -------------------------------------------------------------------------------- /backend/src/entities/Playlist.ts: -------------------------------------------------------------------------------- 1 | import { Entity, BaseEntity, PrimaryGeneratedColumn, Column, ManyToMany } from 'typeorm'; 2 | import User from './User'; 3 | import Track from './Track'; 4 | 5 | @Entity() 6 | export default class Playlist extends BaseEntity { 7 | @PrimaryGeneratedColumn() 8 | id!: number; 9 | 10 | @Column() 11 | name!: string; 12 | 13 | @Column() 14 | createDate!: Date; 15 | 16 | @ManyToMany(() => User, user => user.playlists) 17 | users!: User[]; 18 | 19 | @ManyToMany(() => Track, track => track.playlists) 20 | tracks!: Track[]; 21 | 22 | static findByUserId(id: number) { 23 | return this.createQueryBuilder('playlist') 24 | .innerJoin('playlist.users', 'user') 25 | .innerJoinAndSelect('playlist.tracks', 'track') 26 | .where('user.id = :id', { id }) 27 | .getMany(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /backend/src/entities/Subscribe.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Column, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; 2 | import User from './User'; 3 | import Membership from './Membership'; 4 | 5 | @Entity() 6 | export default class Subscribe { 7 | @PrimaryGeneratedColumn() 8 | id!: number; 9 | 10 | @Column() 11 | startDate!: Date; 12 | 13 | @ManyToOne(() => User, user => user.subscribe) 14 | user!: User; 15 | 16 | @ManyToOne(() => Membership, membership => membership.subscribe) 17 | membership!: Membership; 18 | } 19 | -------------------------------------------------------------------------------- /backend/src/entities/Track.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Entity, 3 | BaseEntity, 4 | PrimaryGeneratedColumn, 5 | Column, 6 | OneToMany, 7 | ManyToOne, 8 | ManyToMany, 9 | JoinTable, 10 | } from 'typeorm'; 11 | import MP3 from './MP3'; 12 | import User from './User'; 13 | import Artist from './Artist'; 14 | import Playlist from './Playlist'; 15 | import Album from './Album'; 16 | import Mag from './Mag'; 17 | 18 | @Entity() 19 | export default class Track extends BaseEntity { 20 | @PrimaryGeneratedColumn() 21 | id!: number; 22 | 23 | @Column({ unique: true }) 24 | title!: string; 25 | 26 | @Column() 27 | songwriter!: string; 28 | 29 | @Column() 30 | composer!: string; 31 | 32 | @Column() 33 | isLocal!: boolean; 34 | 35 | @ManyToOne(() => Album, album => album.tracks) 36 | album!: Album; 37 | 38 | @ManyToOne(() => Mag, mag => mag.tracks) 39 | mag!: Mag; 40 | 41 | @OneToMany(() => MP3, mp3 => mp3.user, { onDelete: 'CASCADE' }) 42 | mp3!: MP3[]; 43 | 44 | @ManyToMany(() => Artist, artist => artist.tracks, { onDelete: 'CASCADE' }) 45 | artists!: Artist[]; 46 | 47 | @ManyToMany(() => User, user => user.tracks) 48 | users!: User[]; 49 | 50 | @ManyToMany(() => Playlist, playlist => playlist.tracks, { onDelete: 'CASCADE' }) 51 | @JoinTable({ name: 'TrackPlaylist' }) 52 | playlists!: Playlist[]; 53 | 54 | static findByUserId(id: number) { 55 | return this.createQueryBuilder('track') 56 | .innerJoin('track.users', 'user') 57 | .leftJoinAndSelect('track.album', 'album') 58 | .leftJoinAndSelect('track.artists', 'artist') 59 | .where('user.id = :id', { id }) 60 | .getMany(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /backend/src/middlewares/auth.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import * as jwt from 'jsonwebtoken'; 3 | import Log from '../models/Log'; 4 | 5 | export interface IJwtPayload { 6 | id: number; 7 | nickname: string; 8 | email: string; 9 | profileURL: string; 10 | } 11 | 12 | export const authenticateWithJwt = async ( 13 | req: Request, 14 | res: Response, 15 | next: NextFunction, 16 | ): Promise => { 17 | try { 18 | const token = req.headers.authorization; 19 | // TODO: redirect 시킬 지 논의해보기 20 | if (!token) return res.redirect(process.env.SERVICE_URL as string); 21 | const user = jwt.verify(token as string, process.env.JWT_SECRET as string) as IJwtPayload; 22 | req.user = user; 23 | next(); 24 | } catch (err) { 25 | // TODO: redirect 시킬 지 논의해보기 26 | res.status(401).json({ success: false, message: 'Invalid Token' }); 27 | } 28 | }; 29 | 30 | export const tryAuthenticateWithJwt = (req: Request, res: Response, next: NextFunction): void => { 31 | try { 32 | const token = req.headers.authorization; 33 | const user = jwt.verify(token as string, process.env.JWT_SECRET as string) as IJwtPayload; 34 | req.user = user; 35 | next(); 36 | } catch (err) { 37 | next(); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /backend/src/models/Log.ts: -------------------------------------------------------------------------------- 1 | import { model, Schema, Model, Document } from 'mongoose'; 2 | 3 | interface ILog extends Document { 4 | eventTime?: Date; 5 | eventName?: string; 6 | parameters?: any; 7 | userInfo?: any; 8 | } 9 | 10 | const EventParams = new Schema({ 11 | prev: String, 12 | next: String, 13 | component: String, 14 | button: String, 15 | type: String, 16 | id: Number, 17 | log: String, 18 | page: String, 19 | action: String, 20 | target: String, 21 | view: String, 22 | }); 23 | 24 | const UserInfoParams = new Schema({ 25 | isLoggedIn: Boolean, 26 | user: Number, 27 | }); 28 | 29 | const LogSchema: Schema = new Schema({ 30 | eventTime: Date, 31 | eventName: String, 32 | parameters: EventParams, 33 | userInfo: UserInfoParams, 34 | userAgent: String, 35 | }); 36 | 37 | const Log: Model = model('Log', LogSchema); 38 | 39 | export default Log; 40 | -------------------------------------------------------------------------------- /backend/src/passport/index.ts: -------------------------------------------------------------------------------- 1 | import passportNaverConfig from './naver'; 2 | 3 | const passportConfig = (): void => { 4 | passportNaverConfig(); 5 | }; 6 | 7 | export default passportConfig; 8 | -------------------------------------------------------------------------------- /backend/src/passport/naver.ts: -------------------------------------------------------------------------------- 1 | import * as passport from 'passport'; 2 | import { Strategy as NaverStrategy } from 'passport-naver'; 3 | import User from '../entities/User'; 4 | import Log from '../models/Log'; 5 | 6 | const passportNaverConfig = (): void => { 7 | const naverStrategyOptions = { 8 | clientID: process.env.NAVER_CLIENT_ID as string, 9 | clientSecret: process.env.NAVER_CLIENT_SECRET as string, 10 | callbackURL: process.env.NAVER_CALLBACK_URL as string, 11 | }; 12 | 13 | // Provider의 토큰은 사용하지 않음 (_ : accessToken __ : refreshToken) 14 | const naverVerify = async (_: string, __: string, profile: any, done: any): Promise => { 15 | try { 16 | const { email, nickname, profile_image: profileURL, age } = profile._json; 17 | let user = await User.findOne({ email }); 18 | if (!user) { 19 | user = new User(); 20 | user.email = email; 21 | user.username = nickname; 22 | user.nickname = nickname; 23 | user.profileURL = profileURL; 24 | user.age = age; 25 | await user.save(); 26 | const logInfo = { eventTime: new Date(), eventName: 'sign_up_event' }; 27 | const log = new Log(logInfo); 28 | console.log('-------------------- log : ', log); 29 | await log.save(); 30 | } 31 | return done(null, user); 32 | } catch (err) { 33 | return done(err); 34 | } 35 | }; 36 | passport.use('naver', new NaverStrategy(naverStrategyOptions, naverVerify)); 37 | }; 38 | 39 | export default passportNaverConfig; 40 | -------------------------------------------------------------------------------- /backend/src/route/album/controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import * as albumService from '../../services/album'; 3 | 4 | const getAlbums = async (req: Request, res: Response, next: NextFunction): Promise => { 5 | try { 6 | const album = await albumService.getAlbums(); 7 | if (!album) return res.status(404).json({ message: 'Album Not Found' }); 8 | return res.status(200).json({ success: true, data: album }); 9 | } catch (err) { 10 | console.error(err); 11 | return next(err); 12 | } 13 | }; 14 | 15 | const getAlbumByAlbumId = async (req: Request, res: Response, next: NextFunction): Promise => { 16 | try { 17 | const { albumId } = req.params; 18 | const album = await albumService.getAlbumByAlbumId(parseInt(albumId, 10)); 19 | if (!album) return res.status(404).json({ message: 'Album Not Found' }); 20 | return res.status(200).json({ success: true, data: album }); 21 | } catch (err) { 22 | console.error(err); 23 | return next(err); 24 | } 25 | }; 26 | 27 | export { getAlbums, getAlbumByAlbumId }; 28 | -------------------------------------------------------------------------------- /backend/src/route/album/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import { getAlbums, getAlbumByAlbumId } from './controller'; 3 | 4 | const route = express.Router(); 5 | 6 | route.get('/', getAlbums); 7 | route.get('/:albumId', getAlbumByAlbumId); 8 | 9 | export default route; 10 | -------------------------------------------------------------------------------- /backend/src/route/artist/controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import * as artistService from '../../services/artist'; 3 | 4 | const getArtists = async (req: Request, res: Response, next: NextFunction): Promise => { 5 | try { 6 | const artists = await artistService.getArtists(); 7 | if (!artists) return res.status(404).json({ message: 'Artist Not Found' }); 8 | return res.status(200).json({ success: true, data: artists }); 9 | } catch (err) { 10 | console.error(err); 11 | return next(err); 12 | } 13 | }; 14 | 15 | const getArtistByArtistId = async ( 16 | req: Request, 17 | res: Response, 18 | next: NextFunction, 19 | ): Promise => { 20 | try { 21 | const { artistId } = req.params; 22 | const artist = await artistService.getArtistByArtistId(parseInt(artistId, 10)); 23 | if (!artist) return res.status(404).json({ message: 'Artist Not Found' }); 24 | return res.status(200).json({ success: true, data: artist }); 25 | } catch (err) { 26 | console.error(err); 27 | return next(err); 28 | } 29 | }; 30 | 31 | export { getArtists, getArtistByArtistId }; 32 | -------------------------------------------------------------------------------- /backend/src/route/artist/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import { getArtists, getArtistByArtistId } from './controller'; 3 | 4 | const route = express.Router(); 5 | 6 | route.get('/', getArtists); 7 | route.get('/:artistId', getArtistByArtistId); 8 | 9 | export default route; 10 | -------------------------------------------------------------------------------- /backend/src/route/auth/controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import { IJwtPayload } from '../../middlewares/auth'; 3 | import * as authService from '../../services/auth'; 4 | 5 | const getToken = (req: Request, res: Response): void => { 6 | const token = authService.createToken(req.user as IJwtPayload); 7 | if ( 8 | req.headers['user-agent']?.includes('iPhone') || 9 | req.headers['user-agent']?.includes('iPad') 10 | ) { 11 | return res.redirect(`minivibe://token?${token}`); // 모바일 12 | } 13 | res.cookie('token', token, { maxAge: 1000 * 60 * 60 * 24 * 1, httpOnly: false }); 14 | return res.redirect(process.env.SERVICE_URL as string); // 웹 15 | }; 16 | 17 | export { getToken }; 18 | -------------------------------------------------------------------------------- /backend/src/route/auth/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import * as passport from 'passport'; 3 | import { getToken } from './controller'; 4 | 5 | const route = express.Router(); 6 | 7 | route.get('/login', passport.authenticate('naver', { session: false })); 8 | route.get('/login/callback', passport.authenticate('naver', { session: false }), getToken); 9 | 10 | export default route; 11 | -------------------------------------------------------------------------------- /backend/src/route/auth2/controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import { IJwtPayload } from '../../middlewares/auth'; 3 | import * as authService from '../../services/auth'; 4 | 5 | const getToken = (req: Request, res: Response): void => { 6 | // 모바일 7 | const token = authService.createToken(req.user as IJwtPayload); 8 | return res.redirect(`minivibe://token?${token}`); 9 | }; 10 | 11 | export { getToken }; 12 | -------------------------------------------------------------------------------- /backend/src/route/auth2/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import * as passport from 'passport'; 3 | import { getToken } from './controller'; 4 | 5 | const route = express.Router(); 6 | 7 | route.get('/login', passport.authenticate('naver', { session: false })); 8 | route.get('/login/callback', passport.authenticate('naver', { session: false }), getToken); 9 | 10 | export default route; 11 | -------------------------------------------------------------------------------- /backend/src/route/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import sampleRoute from './sample'; 3 | import userRoute from './users'; 4 | import logRoute from './log'; 5 | import authRoute from './auth'; 6 | import auth2Route from './auth2'; 7 | import libraryRoute from './library'; 8 | import trackRoute from './track'; 9 | import albumRoute from './album'; 10 | import artistRoute from './artist'; 11 | import magRoute from './mag'; 12 | import playlistRoute from './playlist'; 13 | import { authenticateWithJwt, tryAuthenticateWithJwt } from '../middlewares/auth'; 14 | 15 | const route = express.Router(); 16 | 17 | route.use('/sample', sampleRoute); 18 | route.use('/user', userRoute); 19 | route.use('/log', tryAuthenticateWithJwt, logRoute); 20 | route.use('/auth', authRoute); 21 | route.use('/auth2', auth2Route); 22 | route.use('/library', authenticateWithJwt, libraryRoute); 23 | route.use('/track', trackRoute); 24 | route.use('/album', albumRoute); 25 | route.use('/artist', artistRoute); 26 | route.use('/magazine', magRoute); 27 | route.use('/playlist', playlistRoute); 28 | 29 | export default route; 30 | -------------------------------------------------------------------------------- /backend/src/route/library/album/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import * as passport from 'passport'; 3 | import { getAlbumsByUserId, addAlbum, deleteAlbum } from './controller'; 4 | 5 | const route = express.Router(); 6 | 7 | route.get('/', getAlbumsByUserId); 8 | route.post('/', addAlbum); 9 | route.delete('/:albumId', deleteAlbum); 10 | 11 | export default route; 12 | -------------------------------------------------------------------------------- /backend/src/route/library/artist/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import * as passport from 'passport'; 3 | import { getArtistsByUserId, addArtist, deleteArtist } from './controller'; 4 | 5 | const route = express.Router(); 6 | 7 | route.get('/', getArtistsByUserId); 8 | route.post('/', addArtist); 9 | route.delete('/:artistId', deleteArtist); 10 | 11 | export default route; 12 | -------------------------------------------------------------------------------- /backend/src/route/library/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import albumRoute from './album'; 3 | import trackRoute from './track'; 4 | import artistRoute from './artist'; 5 | import playlistRoute from './playlist'; 6 | 7 | const route = express.Router(); 8 | 9 | route.use('/albums', albumRoute); 10 | route.use('/tracks', trackRoute); 11 | route.use('/artists', artistRoute); 12 | route.use('/playlists', playlistRoute); 13 | 14 | export default route; 15 | -------------------------------------------------------------------------------- /backend/src/route/library/playlist/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import * as passport from 'passport'; 3 | import { getPlaylistsByUserId, addPlaylist, deletePlaylist } from './controller'; 4 | 5 | const route = express.Router(); 6 | 7 | route.get('/', getPlaylistsByUserId); 8 | route.post('/', addPlaylist); 9 | route.delete('/:playlistId', deletePlaylist); 10 | 11 | export default route; 12 | -------------------------------------------------------------------------------- /backend/src/route/library/track/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import * as passport from 'passport'; 3 | import { getTracksByUserId, addTrack, deleteTrack } from './controller'; 4 | 5 | const route = express.Router(); 6 | 7 | route.get('/', getTracksByUserId); 8 | route.post('/', addTrack); 9 | route.delete('/:trackId', deleteTrack); 10 | 11 | export default route; 12 | -------------------------------------------------------------------------------- /backend/src/route/log/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import { createBulkLogs, createLog, getLogs } from './controller'; 3 | 4 | const route = express.Router(); 5 | 6 | route.get('/', getLogs); 7 | route.post('/', createLog); 8 | route.post('/bulk', createBulkLogs); 9 | 10 | export default route; 11 | -------------------------------------------------------------------------------- /backend/src/route/mag/controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import * as magazineService from '../../services/mag'; 3 | 4 | const getMagazines = async (req: Request, res: Response, next: NextFunction): Promise => { 5 | try { 6 | const mag = await magazineService.getMagazines(); 7 | if (!mag) return res.status(404).json({ message: 'Mag Not Found' }); 8 | return res.status(200).json({ success: true, data: mag }); 9 | } catch (err) { 10 | console.error(err); 11 | return next(err); 12 | } 13 | }; 14 | 15 | const getMagazineByMagazineId = async ( 16 | req: Request, 17 | res: Response, 18 | next: NextFunction, 19 | ): Promise => { 20 | try { 21 | const { magId } = req.params; 22 | const mag = await magazineService.getMagazineByMagazineId(parseInt(magId, 10)); 23 | if (!mag) return res.status(404).json({ message: 'Magazine Not Found' }); 24 | return res.status(200).json({ success: true, data: mag }); 25 | } catch (err) { 26 | console.error(err); 27 | return next(err); 28 | } 29 | }; 30 | 31 | export { getMagazines, getMagazineByMagazineId }; 32 | -------------------------------------------------------------------------------- /backend/src/route/mag/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import { getMagazines, getMagazineByMagazineId } from './controller'; 3 | 4 | const route = express.Router(); 5 | 6 | route.get('/', getMagazines); 7 | route.get('/:magId', getMagazineByMagazineId); 8 | 9 | export default route; 10 | -------------------------------------------------------------------------------- /backend/src/route/playlist/controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import * as playlistService from '../../services/playlist'; 3 | 4 | const getPlaylists = async (req: Request, res: Response, next: NextFunction): Promise => { 5 | try { 6 | const playlists = await playlistService.getPlaylists(); 7 | if (!playlists) return res.status(404).json({ message: 'Playlist Not Found' }); 8 | return res.status(200).json({ success: true, data: playlists }); 9 | } catch (err) { 10 | console.error(err); 11 | return next(err); 12 | } 13 | }; 14 | 15 | const getPlaylistByPlaylistId = async ( 16 | req: Request, 17 | res: Response, 18 | next: NextFunction, 19 | ): Promise => { 20 | try { 21 | const { playlistId } = req.params; 22 | const playlist = await playlistService.getPlaylistByPlaylistId(parseInt(playlistId, 10)); 23 | if (!playlist) return res.status(404).json({ message: 'Playlist Not Found' }); 24 | return res.status(200).json({ success: true, data: playlist }); 25 | } catch (err) { 26 | console.error(err); 27 | return next(err); 28 | } 29 | }; 30 | 31 | export { getPlaylists, getPlaylistByPlaylistId }; 32 | -------------------------------------------------------------------------------- /backend/src/route/playlist/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import { getPlaylists, getPlaylistByPlaylistId } from './controller'; 3 | 4 | const route = express.Router(); 5 | 6 | route.get('/', getPlaylists); 7 | route.get('/:playlistId', getPlaylistByPlaylistId); 8 | 9 | export default route; 10 | -------------------------------------------------------------------------------- /backend/src/route/sample/controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | 3 | const sampleGet = (req: Request, res: Response): void => { 4 | const { query, cookies } = req; 5 | console.log(query, cookies); 6 | res.json({ query, cookies }); 7 | }; 8 | 9 | const samplePost = (req: Request, res: Response): void => { 10 | const { body, cookies } = req; 11 | console.log(body, cookies); 12 | res.json({ body, cookies }); 13 | }; 14 | 15 | export { sampleGet, samplePost }; 16 | -------------------------------------------------------------------------------- /backend/src/route/sample/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import { authenticateWithJwt } from '../../middlewares/auth'; 3 | import { sampleGet, samplePost } from './controller'; 4 | 5 | const route = express.Router(); 6 | 7 | // test API: /GET 8 | route.get('/', sampleGet); 9 | 10 | // test API with JWT: /GET 11 | route.get('/jwt', authenticateWithJwt, sampleGet); 12 | 13 | // test API: /POST 14 | route.post('/', samplePost); 15 | 16 | export default route; 17 | -------------------------------------------------------------------------------- /backend/src/route/track/controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import * as trackService from '../../services/track'; 3 | 4 | const getTracks = async (req: Request, res: Response, next: NextFunction): Promise => { 5 | try { 6 | const tracks = await trackService.getTracks(); 7 | if (!tracks) return res.status(404).json({ message: 'Tracks Not Found' }); 8 | return res.status(200).json({ success: true, data: tracks }); 9 | } catch (err) { 10 | console.error(err); 11 | return next(err); 12 | } 13 | }; 14 | 15 | const getTrackByTrackId = async (req: Request, res: Response, next: NextFunction): Promise => { 16 | try { 17 | const { trackId } = req.params; 18 | const track = await trackService.getTrackByTrackId(parseInt(trackId, 10)); 19 | if (!track) return res.status(404).json({ message: 'Track Not Found' }); 20 | return res.status(200).json({ success: true, data: track }); 21 | } catch (err) { 22 | console.error(err); 23 | return next(err); 24 | } 25 | }; 26 | 27 | export { getTracks, getTrackByTrackId }; 28 | -------------------------------------------------------------------------------- /backend/src/route/track/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import { getTracks, getTrackByTrackId } from './controller'; 3 | 4 | const route = express.Router(); 5 | 6 | route.get('/', getTracks); 7 | route.get('/:trackId', getTrackByTrackId); 8 | 9 | export default route; 10 | -------------------------------------------------------------------------------- /backend/src/route/users/controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | // import User from '../../entities/User'; 3 | 4 | const getUser = async (req: Request, res: Response, next: NextFunction): Promise => { 5 | try { 6 | const { user } = req; 7 | if (!user) return res.status(500).json({ success: false }); 8 | return res.status(200).json({ success: true, user }); 9 | } catch (err) { 10 | console.error(err); 11 | return next(err); 12 | } 13 | }; 14 | 15 | export { getUser }; 16 | -------------------------------------------------------------------------------- /backend/src/route/users/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import { authenticateWithJwt } from '../../middlewares/auth'; 3 | import { getUser } from './controller'; 4 | 5 | const route = express.Router(); 6 | 7 | route.get('/', authenticateWithJwt, getUser); 8 | 9 | export default route; 10 | -------------------------------------------------------------------------------- /backend/src/services/album/index.ts: -------------------------------------------------------------------------------- 1 | import Album from '../../entities/Album'; 2 | 3 | const getAlbums = (): Promise => { 4 | // return Album.find(); 5 | return Album.createQueryBuilder('album') 6 | .leftJoinAndSelect('album.tracks', 'track') 7 | .leftJoinAndSelect('track.artists', 'artist') 8 | .leftJoinAndSelect('artist.genres', 'genre') 9 | .getMany(); 10 | }; 11 | 12 | const getAlbumByAlbumId = (albumId: number): Promise => { 13 | // return Album.findOne(albumId, { relations: ['genres', 'artists', 'tracks'] }); 14 | return Album.createQueryBuilder('album') 15 | .leftJoinAndSelect('album.tracks', 'track') 16 | .leftJoinAndSelect('track.artists', 'artist') 17 | .leftJoinAndSelect('artist.genres', 'genre') 18 | .where('album.id = :albumId', { albumId }) 19 | .getOne(); 20 | }; 21 | 22 | export { getAlbums, getAlbumByAlbumId }; 23 | -------------------------------------------------------------------------------- /backend/src/services/artist/index.ts: -------------------------------------------------------------------------------- 1 | import Artist from '../../entities/Artist'; 2 | 3 | const getArtists = (): Promise => { 4 | return Artist.find(); 5 | }; 6 | 7 | const getArtistByArtistId = (artistId: number): Promise => { 8 | // return Artist.findOne(artistId, { relations: ['genres', 'tracks', 'albums'] }); 9 | return Artist.createQueryBuilder('artist') 10 | .leftJoinAndSelect('artist.genres', 'genre') 11 | .leftJoinAndSelect('artist.tracks', 'track') 12 | .leftJoinAndSelect('artist.albums', 'albums') 13 | .leftJoinAndSelect('track.album', 'album') 14 | .where('artist.id = :artistId', { artistId }) 15 | .getOne(); 16 | }; 17 | 18 | export { getArtists, getArtistByArtistId }; 19 | -------------------------------------------------------------------------------- /backend/src/services/auth/index.ts: -------------------------------------------------------------------------------- 1 | import * as jwt from 'jsonwebtoken'; 2 | import { IJwtPayload } from '../../middlewares/auth'; 3 | 4 | const createToken = ({ id, nickname, email, profileURL }: IJwtPayload): string => { 5 | const token = jwt.sign({ id, nickname, email, profileURL }, process.env.JWT_SECRET as string, { 6 | noTimestamp: true, 7 | }); 8 | return token; 9 | }; 10 | 11 | export { createToken }; 12 | -------------------------------------------------------------------------------- /backend/src/services/library/album/index.ts: -------------------------------------------------------------------------------- 1 | import Album from '../../../entities/Album'; 2 | import User from '../../../entities/User'; 3 | 4 | const getAlbumsByUserId = (userId: number): Promise => { 5 | return Album.findByUserId(userId); 6 | }; 7 | 8 | const addAlbum = async (userId: number, albumId: number): Promise => { 9 | const user = (await User.findOne(userId, { relations: ['albums'] })) as User; 10 | const album = (await Album.findOne(albumId)) as Album; 11 | if (!album) return false; 12 | 13 | user.albums.push(album); 14 | await user.save(); 15 | return true; 16 | }; 17 | 18 | const deleteAlbum = async (userId: number, albumId: number): Promise => { 19 | const user = (await User.findOne(userId, { relations: ['albums'] })) as User; 20 | const albumToRemove = (await Album.findOne(albumId)) as Album; 21 | if (!albumToRemove) return false; 22 | 23 | user.albums = user.albums.filter(album => album.id !== albumToRemove.id); 24 | await user.save(); 25 | return true; 26 | }; 27 | 28 | export { getAlbumsByUserId, addAlbum, deleteAlbum }; 29 | -------------------------------------------------------------------------------- /backend/src/services/library/artist/index.ts: -------------------------------------------------------------------------------- 1 | import User from '../../../entities/User'; 2 | import Artist from '../../../entities/Artist'; 3 | 4 | const getArtistsByUserId = (userId: number): Promise => { 5 | return Artist.findByUserId(userId); 6 | }; 7 | 8 | const addArtist = async (userId: number, artistId: number): Promise => { 9 | const user = (await User.findOne(userId, { relations: ['artists'] })) as User; 10 | const artist = (await Artist.findOne(artistId)) as Artist; 11 | if (!artist) return false; 12 | 13 | user.artists.push(artist); 14 | await user.save(); 15 | return true; 16 | }; 17 | 18 | const deleteArtist = async (userId: number, artistId: number): Promise => { 19 | const user = (await User.findOne(userId, { relations: ['artists'] })) as User; 20 | const artistToRemove = (await Artist.findOne(artistId)) as Artist; 21 | if (!artistToRemove) return false; 22 | 23 | user.artists = user.artists.filter(artist => artist.id !== artistToRemove.id); 24 | await user.save(); 25 | return true; 26 | }; 27 | 28 | export { getArtistsByUserId, addArtist, deleteArtist }; 29 | -------------------------------------------------------------------------------- /backend/src/services/library/playlist/index.ts: -------------------------------------------------------------------------------- 1 | import User from '../../../entities/User'; 2 | import Playlist from '../../../entities/Playlist'; 3 | 4 | const getPlaylistsByUserId = (playlistId: number): Promise => { 5 | return Playlist.findByUserId(playlistId); 6 | }; 7 | 8 | const addPlaylist = async (userId: number, playlistId: number): Promise => { 9 | const user = (await User.findOne(userId, { relations: ['playlists'] })) as User; 10 | const playlist = (await Playlist.findOne(playlistId)) as Playlist; 11 | if (!playlist) return false; 12 | 13 | user.playlists.push(playlist); 14 | await user.save(); 15 | return true; 16 | }; 17 | 18 | const deletePlaylist = async (userId: number, playlistId: number): Promise => { 19 | const user = (await User.findOne(userId, { relations: ['playlists'] })) as User; 20 | const playlistToRemove = (await Playlist.findOne(playlistId)) as Playlist; 21 | if (!playlistToRemove) return false; 22 | 23 | user.playlists = user.playlists.filter(playlist => playlist.id !== playlistToRemove.id); 24 | await user.save(); 25 | return true; 26 | }; 27 | 28 | export { getPlaylistsByUserId, addPlaylist, deletePlaylist }; 29 | -------------------------------------------------------------------------------- /backend/src/services/library/track/index.ts: -------------------------------------------------------------------------------- 1 | import Track from '../../../entities/Track'; 2 | import User from '../../../entities/User'; 3 | 4 | const getTracksByUserId = (userId: number): Promise => { 5 | return Track.findByUserId(userId); 6 | }; 7 | 8 | const addTrack = async (userId: number, trackId: number): Promise => { 9 | const user = (await User.findOne(userId, { relations: ['tracks'] })) as User; 10 | const track = (await Track.findOne(trackId)) as Track; 11 | if (!track) return false; 12 | 13 | user.tracks.push(track); 14 | await user.save(); 15 | return true; 16 | }; 17 | 18 | const deleteTrack = async (userId: number, trackId: number): Promise => { 19 | const user = (await User.findOne(userId, { relations: ['tracks'] })) as User; 20 | const trackToRemove = (await Track.findOne(trackId)) as Track; 21 | if (!trackToRemove) false; 22 | 23 | user.tracks = user.tracks.filter(track => track.id !== trackToRemove.id); 24 | await user.save(); 25 | return true; 26 | }; 27 | 28 | export { getTracksByUserId, addTrack, deleteTrack }; 29 | -------------------------------------------------------------------------------- /backend/src/services/mag/index.ts: -------------------------------------------------------------------------------- 1 | import Mag from '../../entities/Mag'; 2 | 3 | const getMagazines = (): Promise => { 4 | // return Mag.find(); 5 | return Mag.createQueryBuilder('mag') 6 | .leftJoinAndSelect('mag.tracks', 'track') 7 | .leftJoinAndSelect('track.album', 'album') 8 | .leftJoinAndSelect('track.artists', 'artist') 9 | .getMany(); 10 | }; 11 | 12 | const getMagazineByMagazineId = (magazineId: number): Promise => { 13 | // return Mag.findOne(magazineId, { relations: ['tracks'] }); 14 | return Mag.createQueryBuilder('mag') 15 | .leftJoinAndSelect('mag.tracks', 'track') 16 | .leftJoinAndSelect('track.album', 'album') 17 | .leftJoinAndSelect('track.artists', 'artist') 18 | .where('mag.id = :magazineId', { magazineId }) 19 | .getOne(); 20 | }; 21 | 22 | export { getMagazines, getMagazineByMagazineId }; 23 | -------------------------------------------------------------------------------- /backend/src/services/playlist/index.ts: -------------------------------------------------------------------------------- 1 | import Playlist from '../../entities/Playlist'; 2 | 3 | const getPlaylists = (): Promise => { 4 | // return Playlist.find({ relations: ['tracks'] }); 5 | // return Mag.find(); 6 | return Playlist.createQueryBuilder('playlist') 7 | .leftJoinAndSelect('playlist.tracks', 'track') 8 | .leftJoinAndSelect('track.album', 'album') 9 | .leftJoinAndSelect('track.artists', 'artist') 10 | .getMany(); 11 | }; 12 | 13 | const getPlaylistByPlaylistId = (playlistId: number): Promise => { 14 | // return Playlist.findOne(playlistId, { relations: ['tracks'] }); 15 | return Playlist.createQueryBuilder('playlist') 16 | .leftJoinAndSelect('playlist.tracks', 'track') 17 | .leftJoinAndSelect('track.album', 'album') 18 | .leftJoinAndSelect('track.artists', 'artist') 19 | .where('playlist.id = :playlistId', { playlistId }) 20 | .getOne(); 21 | }; 22 | 23 | export { getPlaylists, getPlaylistByPlaylistId }; 24 | -------------------------------------------------------------------------------- /backend/src/services/track/index.ts: -------------------------------------------------------------------------------- 1 | import Track from '../../entities/Track'; 2 | 3 | const getTracks = (): Promise => { 4 | return Track.find({ relations: ['album', 'artists'] }); 5 | }; 6 | 7 | const getTrackByTrackId = (trackId: number): Promise => { 8 | return Track.findOne(trackId, { relations: ['album', 'artists'] }); 9 | }; 10 | 11 | export { getTracks, getTrackByTrackId }; 12 | -------------------------------------------------------------------------------- /frontend/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "next/babel" 4 | ], 5 | "plugins": [ 6 | [ 7 | "styled-components", 8 | { 9 | "ssr": true, 10 | "displayName": true, 11 | "preprocess": false, 12 | } 13 | ] 14 | ] 15 | } -------------------------------------------------------------------------------- /frontend/.env.development: -------------------------------------------------------------------------------- 1 | HOSTNAME=localhost 2 | NEXT_PUBLIC_API_BASE_URL=http://$HOSTNAME:8000/api 3 | NEXT_PUBLIC_NAVER_LOGIN_URL=http://$HOSTNAME:8000/api/auth/login 4 | -------------------------------------------------------------------------------- /frontend/.env.production: -------------------------------------------------------------------------------- 1 | HOSTNAME=115.85.181.152 2 | NEXT_PUBLIC_API_BASE_URL=http://$HOSTNAME:8000/api 3 | NEXT_PUBLIC_NAVER_LOGIN_URL=http://$HOSTNAME:8000/api/auth/login -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Project01-C-User-Event-Collector 2 | 아자아자! 사이수 화이팅 !! (`∇´ゞ 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/images.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.jpg' { 2 | const url: string; 3 | export default url; 4 | } 5 | 6 | declare module '*.svg' { 7 | const url: string; 8 | export default url; 9 | } 10 | 11 | declare module '*.png' { 12 | const url: string; 13 | export default url; 14 | } 15 | 16 | declare module '*.jpg?include' { 17 | const url: string; 18 | export default url; 19 | } 20 | 21 | declare module '*.svg?include' { 22 | const url: string; 23 | export default url; 24 | } 25 | 26 | declare module '*.png?include' { 27 | const url: string; 28 | export default url; 29 | } 30 | 31 | declare module '*.jpg?webp' { 32 | const url: string; 33 | export default url; 34 | } 35 | 36 | declare module '*.svg?webp' { 37 | const url: string; 38 | export default url; 39 | } 40 | 41 | declare module '*.png?webp' { 42 | const url: string; 43 | export default url; 44 | } 45 | 46 | declare module '*.jpg?inline' { 47 | const url: string; 48 | export default url; 49 | } 50 | 51 | declare module '*.svg?inline' { 52 | const url: string; 53 | export default url; 54 | } 55 | 56 | declare module '*.png?inline' { 57 | const url: string; 58 | export default url; 59 | } 60 | 61 | declare module '*.jpg?url' { 62 | const url: string; 63 | export default url; 64 | } 65 | 66 | declare module '*.svg?url' { 67 | const url: string; 68 | export default url; 69 | } 70 | 71 | declare module '*.png?url' { 72 | const url: string; 73 | export default url; 74 | } 75 | -------------------------------------------------------------------------------- /frontend/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | -------------------------------------------------------------------------------- /frontend/next.config.js: -------------------------------------------------------------------------------- 1 | // const withTypescript = require('@zeit/next-typescript'); 2 | const withImages = require('next-images') 3 | 4 | module.exports = withImages({ 5 | async redirects() { 6 | return [ 7 | { 8 | source: '/', 9 | destination: '/today', 10 | permanent: true, 11 | }, 12 | ] 13 | }, 14 | }) 15 | -------------------------------------------------------------------------------- /frontend/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import App from 'next/app'; 2 | import React from 'react'; 3 | import theme from '@styles/themes'; 4 | import GlobalStyles from '@styles/global-styles'; 5 | import { ThemeProvider } from '@styles/themed-components'; 6 | import { AuthProvider } from '@context/AuthContext'; 7 | import { PlayProvider } from '@context/PlayContext'; 8 | import 'semantic-ui-css/semantic.min.css'; 9 | import 'bootstrap/dist/css/bootstrap.css'; 10 | 11 | import Layout from '@components/Layout'; 12 | 13 | class ReactApp extends App { 14 | public render() { 15 | const { Component, pageProps } = this.props; 16 | return ( 17 | <> 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ); 30 | } 31 | } 32 | 33 | export default ReactApp; 34 | -------------------------------------------------------------------------------- /frontend/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Document, { Html, Head, Main, NextScript } from 'next/document'; 3 | import { ServerStyleSheet } from '@styles/themed-components'; 4 | 5 | export default class MyDocument extends Document { 6 | static async getInitialProps(context: any) { 7 | const sheet = new ServerStyleSheet(); 8 | const originalRenderPage = context.renderPage; 9 | 10 | try { 11 | context.renderPage = () => 12 | originalRenderPage({ 13 | enhanceApp: App => props => sheet.collectStyles(), 14 | }); 15 | 16 | const initialProps = await Document.getInitialProps(context); 17 | return { 18 | ...initialProps, 19 | styles: ( 20 | <> 21 | {initialProps.styles} 22 | {sheet.getStyleElement()} 23 | 24 | ), 25 | }; 26 | } finally { 27 | sheet.seal(); 28 | } 29 | } 30 | 31 | render() { 32 | return ( 33 | 34 | 35 | Mini VIBE 36 | 37 | 38 |
39 | 40 | 41 | 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /frontend/pages/album/[id].tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router'; 2 | import api from '@api/index'; 3 | import AlbumDetail from '../../src/pages/Detail/Album'; 4 | 5 | export function Index({ albumData }) { 6 | const router = useRouter(); 7 | interface IMoveEventLog { 8 | eventTime: Date; 9 | eventName: string; 10 | parameters: { 11 | prev: string; 12 | next: string; 13 | }; 14 | } 15 | 16 | const logData: IMoveEventLog = { 17 | eventTime: new Date(), 18 | eventName: 'move_event', 19 | parameters: { prev: '.', next: router.asPath }, 20 | }; 21 | api.post('/log', logData); 22 | 23 | return ( 24 | <> 25 | 26 | 27 | ); 28 | } 29 | 30 | export async function getStaticPaths() { 31 | const data = await api.get(`/album/`).then(res => res.data); 32 | const albumData = data.data; 33 | const paths = albumData.map(album => ({ 34 | params: { id: String(album.id) }, 35 | })); 36 | 37 | return { paths, fallback: false }; 38 | } 39 | 40 | export const getStaticProps = async ({ params }) => { 41 | const data = await api.get(`/album/${params.id}`).then(res => res.data); 42 | const albumData = data.data; 43 | if (!data) { 44 | return { 45 | notfound: true, 46 | }; 47 | } 48 | return { 49 | props: { albumData }, 50 | }; 51 | }; 52 | 53 | export default Index; 54 | -------------------------------------------------------------------------------- /frontend/pages/artist/[id].tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router'; 2 | import api from '@api/index'; 3 | import ArtistDetail from '../../src/pages/Detail/Artist'; 4 | 5 | export function Index({ artistData }) { 6 | const router = useRouter(); 7 | interface IMoveEventLog { 8 | eventTime: Date; 9 | eventName: string; 10 | parameters: { 11 | prev: string; 12 | next: string; 13 | }; 14 | } 15 | 16 | const logData: IMoveEventLog = { 17 | eventTime: new Date(), 18 | eventName: 'move_event', 19 | parameters: { prev: '.', next: router.asPath }, 20 | }; 21 | api.post('/log', logData); 22 | 23 | return ( 24 | <> 25 | 26 | 27 | ); 28 | } 29 | 30 | export async function getStaticPaths() { 31 | const data = await api.get(`/artist/`).then(res => res.data); 32 | const artistData = data.data; 33 | const paths = artistData.map(artist => ({ 34 | params: { id: String(artist.id) }, 35 | })); 36 | 37 | return { paths, fallback: false }; 38 | } 39 | 40 | export const getStaticProps = async ({ params }) => { 41 | const data = await api.get(`/artist/${params.id}`).then(res => res.data); 42 | const artistData = data.data; 43 | if (!data) { 44 | return { 45 | notfound: true, 46 | }; 47 | } 48 | return { 49 | props: { artistData }, 50 | }; 51 | }; 52 | 53 | export default Index; 54 | -------------------------------------------------------------------------------- /frontend/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Sample from '@components/sample-rx'; 2 | 3 | const Index = () => ( 4 |
5 | 6 |
7 | ); 8 | 9 | export default Index; 10 | -------------------------------------------------------------------------------- /frontend/pages/magazines/[id].tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router'; 2 | import api from '@api/index'; 3 | import MegazineDetail from '../../src/pages/Detail/Magazine'; 4 | 5 | export function Index({ magazineData }) { 6 | const router = useRouter(); 7 | interface IMoveEventLog { 8 | eventTime: Date; 9 | eventName: string; 10 | parameters: { 11 | prev: string; 12 | next: string; 13 | }; 14 | } 15 | 16 | const logData: IMoveEventLog = { 17 | eventTime: new Date(), 18 | eventName: 'move_event', 19 | parameters: { prev: '.', next: router.asPath }, 20 | }; 21 | api.post('/log', logData); 22 | 23 | return ( 24 | <> 25 | 26 | 27 | ); 28 | } 29 | 30 | export async function getStaticPaths() { 31 | const data = await api.get(`/magazine/`).then(res => res.data); 32 | const magazineData = data.data; 33 | const paths = magazineData.map(magazine => ({ 34 | params: { id: String(magazine.id) }, 35 | })); 36 | 37 | return { paths, fallback: false }; 38 | } 39 | 40 | export const getStaticProps = async ({ params }) => { 41 | const data = await api.get(`/magazine/${params.id}`).then(res => res.data); 42 | const magazineData = data.data; 43 | if (!data) { 44 | return { 45 | notfound: true, 46 | }; 47 | } 48 | return { 49 | props: { magazineData }, 50 | }; 51 | }; 52 | 53 | export default Index; 54 | -------------------------------------------------------------------------------- /frontend/pages/playlist/[id].tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router'; 2 | import api from '@api/index'; 3 | import PlaylistDetail from '../../src/pages/Detail/Playlist'; 4 | 5 | export function Index({ playlistData }) { 6 | const router = useRouter(); 7 | interface IMoveEventLog { 8 | eventTime: Date; 9 | eventName: string; 10 | parameters: { 11 | prev: string; 12 | next: string; 13 | }; 14 | } 15 | 16 | const logData: IMoveEventLog = { 17 | eventTime: new Date(), 18 | eventName: 'move_event', 19 | parameters: { prev: '.', next: router.asPath }, 20 | }; 21 | api.post('/log', logData); 22 | 23 | return ( 24 | <> 25 | 26 | 27 | ); 28 | } 29 | 30 | export async function getStaticPaths() { 31 | const data = await api.get(`/playlist/`).then(res => res.data); 32 | const playlistData = data.data; 33 | const paths = playlistData.map(playlist => ({ 34 | params: { id: String(playlist.id) }, 35 | })); 36 | 37 | return { paths, fallback: false }; 38 | } 39 | 40 | export const getStaticProps = async ({ params }) => { 41 | const data = await api.get(`/playlist/${params.id}`).then(res => res.data); 42 | const playlistData = data.data; 43 | if (!data) { 44 | return { 45 | notfound: true, 46 | }; 47 | } 48 | return { 49 | props: { playlistData }, 50 | }; 51 | }; 52 | 53 | export default Index; 54 | -------------------------------------------------------------------------------- /frontend/pages/track/[id].tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router'; 2 | import api from '@api/index'; 3 | import TrackDetail from '../../src/pages/Detail/Track'; 4 | 5 | export function Index({ trackData }) { 6 | const router = useRouter(); 7 | interface IMoveEventLog { 8 | eventTime: Date; 9 | eventName: string; 10 | parameters: { 11 | prev: string; 12 | next: string; 13 | }; 14 | } 15 | const logData: IMoveEventLog = { 16 | eventTime: new Date(), 17 | eventName: 'move_event', 18 | parameters: { prev: '.', next: router.asPath }, 19 | }; 20 | api.post('/log', logData); 21 | 22 | return ( 23 | <> 24 | 25 | 26 | ); 27 | } 28 | 29 | export async function getStaticPaths() { 30 | const data = await api.get(`/track/`).then(res => res.data); 31 | const trackData = data.data; 32 | const paths = trackData.map(track => ({ 33 | params: { id: String(track.id) }, 34 | })); 35 | 36 | return { paths, fallback: false }; 37 | } 38 | 39 | export const getStaticProps = async ({ params }) => { 40 | const data = await api.get(`/track/${params.id}`).then(res => res.data); 41 | const trackData = data.data; 42 | if (!data) { 43 | return { 44 | notfound: true, 45 | }; 46 | } 47 | return { 48 | props: { trackData }, 49 | }; 50 | }; 51 | 52 | export default Index; 53 | -------------------------------------------------------------------------------- /frontend/public/images/banner-ad-img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-C-User-Event-Collector/b6c6b380402293efd8cbae773a25a7e75644d71d/frontend/public/images/banner-ad-img.png -------------------------------------------------------------------------------- /frontend/public/images/empty-music-img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-C-User-Event-Collector/b6c6b380402293efd8cbae773a25a7e75644d71d/frontend/public/images/empty-music-img.png -------------------------------------------------------------------------------- /frontend/public/images/header-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-C-User-Event-Collector/b6c6b380402293efd8cbae773a25a7e75644d71d/frontend/public/images/header-logo.png -------------------------------------------------------------------------------- /frontend/public/images/modal-img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-C-User-Event-Collector/b6c6b380402293efd8cbae773a25a7e75644d71d/frontend/public/images/modal-img.png -------------------------------------------------------------------------------- /frontend/public/images/spinnerSmallCat.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-C-User-Event-Collector/b6c6b380402293efd8cbae773a25a7e75644d71d/frontend/public/images/spinnerSmallCat.gif -------------------------------------------------------------------------------- /frontend/public/images/track-hover-img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-C-User-Event-Collector/b6c6b380402293efd8cbae773a25a7e75644d71d/frontend/public/images/track-hover-img.png -------------------------------------------------------------------------------- /frontend/src/api/index.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const api = axios.create({ 4 | baseURL: process.env.NEXT_PUBLIC_API_BASE_URL, 5 | validateStatus(status) { 6 | return status < 500; 7 | }, 8 | }); 9 | export default api; 10 | -------------------------------------------------------------------------------- /frontend/src/components/AlbumList/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@styles/themed-components'; 3 | 4 | import AlbumCard from '@components/Common/Card/AlbumCard'; 5 | 6 | const AlbumList = ({ albumList }) => { 7 | return ( 8 | <> 9 | 10 | {albumList 11 | ? albumList?.map(album => ) 12 | : null} 13 | 14 | 15 | ); 16 | }; 17 | 18 | const ListContainer = styled.div` 19 | display: grid; 20 | grid-template-columns: repeat( 21 | auto-fill, 22 | minmax(${props => props.theme.size.smallCarouselContentWidth}, 1fr) 23 | ); 24 | grid-gap: 65px 10px; 25 | `; 26 | 27 | export default AlbumList; 28 | -------------------------------------------------------------------------------- /frontend/src/components/ArtistList/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@styles/themed-components'; 3 | import ArtistCard from '@components/Common/Card/ArtistCard'; 4 | import { mutate } from 'swr'; 5 | import api from '@api/index'; 6 | import { useAuthDispatch } from '@context/AuthContext'; 7 | 8 | const ArtistList = ({ artistList }) => { 9 | const dispatch = useAuthDispatch(); 10 | 11 | const fetchData = async id => { 12 | await api.delete(`library/artists/${id}`); 13 | dispatch({ type: 'DELETE_ARTIST', artistId: id }); 14 | const updatedArtists = await artistList.filter(artist => artist.id !== id); 15 | mutate( 16 | '/library/artists', 17 | data => { 18 | return { ...data, data: updatedArtists }; 19 | }, 20 | false, 21 | ); 22 | }; 23 | 24 | const deleteArtist = (e, id) => { 25 | fetchData(id); 26 | }; 27 | 28 | return ( 29 | 30 | {artistList 31 | ? artistList?.map(artist => ( 32 | 38 | )) 39 | : null} 40 | 41 | ); 42 | }; 43 | 44 | const ListContainer = styled.div` 45 | display: grid; 46 | grid-template-columns: repeat( 47 | auto-fill, 48 | minmax(${props => props.theme.size.smallCarouselContentWidth}, 1fr) 49 | ); 50 | grid-gap: 45px 0; 51 | `; 52 | 53 | export default ArtistList; 54 | -------------------------------------------------------------------------------- /frontend/src/components/Common/A/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Link from 'next/link'; 3 | import ClickEventWrapper from '@components/EventWrapper/ClickEventWrapper'; 4 | 5 | interface IAProps { 6 | next: string; 7 | target: string; 8 | id?: number; 9 | children: any; 10 | } 11 | 12 | function A({ next, target, id, children }: IAProps) { 13 | return ( 14 | <> 15 | {id ? ( 16 | 17 | 18 | {children} 19 | 20 | 21 | ) : ( 22 | 23 | {children} 24 | 25 | )} 26 | 27 | ); 28 | } 29 | 30 | export default A; 31 | -------------------------------------------------------------------------------- /frontend/src/components/Common/Button/BoxPlayButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | function BoxPlayButton() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | 12 | const Triangle = styled.div` 13 | position: relative; 14 | left: 2px; 15 | border-top: 8px solid transparent; 16 | border-left: 12px solid ${props => props.theme.color.highlight}; 17 | border-bottom: 8px solid transparent; 18 | `; 19 | 20 | const ButtonWrapper = styled.button` 21 | width: 35px; 22 | height: 35px; 23 | border-radius: 50%; 24 | background: white; 25 | opacity: 75%; 26 | transition: 0.5s all; 27 | display: flex; 28 | justify-content: center; 29 | align-items: center; 30 | box-sizing: border-box; 31 | border: none; 32 | position: absolute; 33 | &:hover { 34 | opacity: 100%; 35 | transform: scale(1.1); 36 | } 37 | `; 38 | 39 | export default BoxPlayButton; 40 | -------------------------------------------------------------------------------- /frontend/src/components/Common/Button/CircleHeartButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { IoHeart } from 'react-icons/io5'; 4 | 5 | function CircleHeartButton() { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | } 12 | 13 | const ButtonWrapper = styled.button` 14 | display: flex; 15 | justify-content: center; 16 | align-items: center; 17 | 18 | width: 42px; 19 | height: 42px; 20 | border-radius: 50%; 21 | background: ${props => props.theme.color.mainBGColor}; 22 | color: ${props => props.theme.color.highlight}; 23 | `; 24 | 25 | export default CircleHeartButton; 26 | -------------------------------------------------------------------------------- /frontend/src/components/Common/Card/MagLargeCard/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@styles/themed-components'; 3 | 4 | import TrackItem from '@components/Common/TrackItem'; 5 | import description from '../../../../constants/description'; 6 | 7 | const MagLargeCard = ({ data }) => { 8 | return ( 9 | 10 | 11 | {data.title} 12 | {' '} 13 | < 14 | {data.album.name} 15 | > 16 | 17 | 18 | {description} 19 | 20 | 21 | ); 22 | }; 23 | 24 | const Container = styled.div` 25 | display: flex; 26 | flex-direction: column; 27 | `; 28 | 29 | const Title = styled.h3` 30 | font-size: 1.8rem; 31 | font-weight: 700; 32 | padding: 1rem 0; 33 | `; 34 | 35 | const ImageContainer = styled.img` 36 | object-fit: cover; 37 | width: 100%; 38 | height: auto; 39 | `; 40 | 41 | const Description = styled.div` 42 | font-size: 15px; 43 | font-weight: 400; 44 | color: #333; 45 | line-height: 1.8rem; 46 | padding: 1rem 0; 47 | `; 48 | 49 | export default MagLargeCard; 50 | -------------------------------------------------------------------------------- /frontend/src/components/Common/CircleImage/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@styles/themed-components'; 3 | 4 | const CircleImage = ({ imageSrc, alt }) => { 5 | return ; 6 | }; 7 | 8 | const Container = styled.img` 9 | width: 100%; 10 | height: 100%; 11 | border-radius: 50%; 12 | `; 13 | 14 | export default CircleImage; 15 | -------------------------------------------------------------------------------- /frontend/src/components/Common/MagTag/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | function MagTag({ type }) { 5 | switch (type) { 6 | case 'special': 7 | return ( 8 | 9 | SPECIAL 10 | 11 | ); 12 | case 'pick': 13 | return ( 14 | 15 | PICK 16 | 17 | ); 18 | case 'genre': 19 | return ( 20 | 21 | GENRE 22 | 23 | ); 24 | default: 25 | return ( 26 | 27 | 전체 28 | 29 | ); 30 | } 31 | } 32 | 33 | const TagText = styled.span` 34 | font-size: 13px; 35 | font-weight: 600; 36 | font-style: italic; 37 | position: relative; 38 | bottom: 1px; 39 | color: ${props => props.theme.color.white}; 40 | `; 41 | 42 | const All = styled.a` 43 | padding: 5px 16px; 44 | border-radius: 30px; 45 | background: ${props => props.theme.color.highlight}; 46 | `; 47 | 48 | const SpecialTag = styled.a` 49 | padding: 5px 16px; 50 | border-radius: 30px; 51 | background: linear-gradient(to right, red, #ff00a0, #7e00e4); 52 | `; 53 | 54 | const PickTag = styled.a` 55 | padding: 5px 16px; 56 | border-radius: 30px; 57 | background: ${props => props.theme.color.highlight}; 58 | `; 59 | 60 | const GenreTag = styled.a` 61 | padding: 5px 16px; 62 | border-radius: 30px; 63 | background: #7e00e4; 64 | `; 65 | 66 | export default MagTag; 67 | -------------------------------------------------------------------------------- /frontend/src/components/Common/SampleSection/RelatedArtist.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@styles/themed-components'; 3 | import useFetch from '@hooks/useFetch'; 4 | import Spinner from '@components/Common/Spinner'; 5 | import ArtistCard from '@components/Common/Card/ArtistCard'; 6 | 7 | function RelatedArtist() { 8 | const { data, isLoading, isError } = useFetch(`/artist`, null); 9 | 10 | if (isLoading) return ; 11 | if (isError) { 12 | console.log(isError); 13 | return
...Error
; 14 | } 15 | const artists = data.data; 16 | 17 | return ( 18 | 19 | {artists && 20 | artists.splice(0, 4).map(artist => ( 21 | 22 | 23 | 24 | ))} 25 | 26 | ); 27 | } 28 | 29 | const SectionContentWrapper = styled.div` 30 | width: 100%; 31 | margin-top: 28px; 32 | display: flex; 33 | `; 34 | 35 | const ArtistWrapper = styled.div` 36 | text-align: left; 37 | margin-right: 20px; 38 | `; 39 | 40 | export default React.memo(RelatedArtist); 41 | -------------------------------------------------------------------------------- /frontend/src/components/Common/SampleSection/RelatedPlaylist.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@styles/themed-components'; 3 | import useFetch from '@hooks/useFetch'; 4 | import Spinner from '@components/Common/Spinner'; 5 | import PlaylistCard from '@components/Common/Card/PlaylistCard'; 6 | 7 | function RelatedPlaylist() { 8 | console.log('relatedPlaylist ===='); 9 | const { data, isLoading, isError } = useFetch(`/playlist`, null); 10 | if (isLoading) return ; 11 | if (isError) { 12 | console.log(isError); 13 | return
...Error
; 14 | } 15 | const playlists = data.data; 16 | 17 | return ( 18 | 19 | {playlists && 20 | playlists.splice(0, 3).map(playlist => ( 21 | 22 | 23 | 24 | ))} 25 | 26 | ); 27 | } 28 | 29 | const SectionContentWrapper = styled.div` 30 | width: 100%; 31 | margin-top: 20px; 32 | display: flex; 33 | `; 34 | 35 | const PlaylistWrapper = styled.div` 36 | text-align: left; 37 | margin-right: 20px; 38 | `; 39 | 40 | export default React.memo(RelatedPlaylist); 41 | -------------------------------------------------------------------------------- /frontend/src/components/Common/Section/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@styles/themed-components'; 3 | 4 | const Section = ({ children }) => { 5 | return {children}; 6 | }; 7 | 8 | const Wrapper = styled.div` 9 | width: 100%; 10 | padding: 30px 0 35px 0; 11 | border-bottom: 1px solid ${props => props.theme.color.borderColor}; 12 | .section-title { 13 | ${props => props.theme.font.secTitle} 14 | position : relative; 15 | top: 8px; 16 | } 17 | overflow: visible; 18 | `; 19 | 20 | export default Section; 21 | -------------------------------------------------------------------------------- /frontend/src/components/Common/Spinner/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@styles/themed-components'; 3 | 4 | const Spinner = () => { 5 | return ( 6 | 7 | spinner 8 | 9 | ); 10 | }; 11 | 12 | const Wrapper = styled.div` 13 | width: 100%; 14 | height: 700px; 15 | display: flex; 16 | align-items: center; 17 | justify-content: center; 18 | `; 19 | 20 | const Image = styled.img` 21 | width: 280px; 22 | `; 23 | 24 | export default Spinner; 25 | -------------------------------------------------------------------------------- /frontend/src/components/EventWrapper/ClickEventWrapper.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { useRouter } from 'next/router'; 4 | import api from '@api/index'; 5 | 6 | interface IClickEventLog { 7 | eventName: string; 8 | parameters: { 9 | page: string; 10 | target: string; 11 | }; 12 | } 13 | 14 | interface IClickEventProps { 15 | target: string; 16 | id?: number | null; 17 | children?: any; 18 | } 19 | 20 | interface IEventTargetProps { 21 | onClick?: React.MouseEventHandler; 22 | } 23 | 24 | function ClickEventWrapper({ target, id, children }: IClickEventProps) { 25 | const router = useRouter(); 26 | 27 | const logData: IClickEventLog = { 28 | eventName: 'click_event', 29 | parameters: { page: router.asPath, target: id ? `${target}/${id}` : target }, 30 | }; 31 | 32 | const clickEventHandler = () => { 33 | api.post('/log', { ...logData, eventTime: new Date() }); 34 | }; 35 | 36 | return {children}; 37 | } 38 | 39 | const Wrapper = styled.div` 40 | width: 100%; 41 | cursor: pointer; 42 | `; 43 | 44 | export default ClickEventWrapper; 45 | -------------------------------------------------------------------------------- /frontend/src/components/EventWrapper/LibraryEventWrapper.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import logEventHandler from '@utils/logEventHandler'; 4 | 5 | interface ILibraryEventLog { 6 | eventName: string; 7 | parameters: { 8 | action: string; 9 | type: string; 10 | id: number; 11 | }; 12 | } 13 | 14 | interface ILibraryEventProps { 15 | type: string; 16 | action: string; 17 | id: number; 18 | children?: any; 19 | } 20 | 21 | interface IEventTargetProps { 22 | onClick?: React.MouseEventHandler; 23 | } 24 | 25 | function LibraryEventWrapper({ type, action, id, children }: ILibraryEventProps) { 26 | const logData: ILibraryEventLog = { 27 | eventName: 'library_event', 28 | parameters: { action, type, id }, 29 | }; 30 | 31 | return {children}; 32 | } 33 | 34 | const Wrapper = styled.div` 35 | width: 100%; 36 | cursor: pointer; 37 | `; 38 | 39 | export default LibraryEventWrapper; 40 | -------------------------------------------------------------------------------- /frontend/src/components/Layout/SideBar/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@styles/themed-components'; 3 | import { CgSearch } from 'react-icons/cg'; 4 | import Image from 'next/image'; 5 | import A from '@components/Common/A'; 6 | 7 | function Header() { 8 | return ( 9 | 10 | 11 | 12 | header-logo 13 | 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | 22 | const Container = styled.div` 23 | display: flex; 24 | justify-content: space-between; 25 | align-items: center; 26 | `; 27 | 28 | const ImageWrapper = styled.div` 29 | cursor: pointer; 30 | `; 31 | 32 | const IconWrapper = styled.div` 33 | cursor: pointer; 34 | display: flex; 35 | height: 30px; 36 | align-items: flex-start; 37 | &:hover { 38 | color: ${props => props.theme.color.white}; 39 | } 40 | `; 41 | 42 | export default Header; 43 | -------------------------------------------------------------------------------- /frontend/src/components/Layout/SideBar/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@styles/themed-components'; 3 | import NavBar from './NavBar'; 4 | import Header from './Header'; 5 | 6 | function Sidebar() { 7 | return ( 8 | 9 |
10 | 11 | 12 | ); 13 | } 14 | 15 | const Container = styled.header` 16 | width: ${props => props.theme.size.sidebarWidth}; 17 | top: 0; 18 | bottom: 81px; 19 | left: 0; 20 | position: fixed; 21 | z-index: 10100; 22 | background: ${props => props.theme.color.black}; 23 | color: ${props => props.theme.color.headerNavColor}; 24 | padding: 1rem; 25 | `; 26 | 27 | export default Sidebar; 28 | -------------------------------------------------------------------------------- /frontend/src/components/Layout/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@styles/themed-components'; 3 | import SideBar from '@components/Layout/SideBar'; 4 | import PlayBar from '@components/Layout/PlayBar'; 5 | import Footer from '@components/Layout/Footer'; 6 | import useLogError from '@hooks/useLogError'; 7 | 8 | function Layout({ children }) { 9 | useLogError(); 10 | 11 | return ( 12 | 13 | 14 | 15 | {children} 16 | 17 |