├── .github └── pull_request_template.md ├── .gitignore ├── README.md ├── backend ├── .dockerignore ├── .eslintignore ├── .eslintrc.json ├── Dockerfile ├── app.ts ├── controllers │ ├── albums │ │ └── index.ts │ ├── artists │ │ └── index.ts │ ├── auth │ │ ├── index.ts │ │ ├── local │ │ │ ├── login.ts │ │ │ └── signup.ts │ │ └── naver │ │ │ ├── callbackProcess.ts │ │ │ └── redirection.ts │ ├── dj-stations │ │ └── index.ts │ ├── genres │ │ └── index.ts │ ├── library │ │ ├── albums.ts │ │ ├── artists.ts │ │ ├── index.ts │ │ ├── playlsits.ts │ │ └── tracks.ts │ ├── log │ │ ├── iOS.ts │ │ ├── index.ts │ │ └── web.ts │ ├── magazines │ │ └── index.ts │ ├── news │ │ └── index.ts │ ├── playlists │ │ └── index.ts │ ├── search │ │ ├── getAlbums.ts │ │ ├── getAll.ts │ │ ├── getArtists.ts │ │ ├── getTracks.ts │ │ └── index.ts │ ├── tracks │ │ └── index.ts │ └── users │ │ ├── index.ts │ │ ├── userLikedItems │ │ └── index.ts │ │ └── userProfile │ │ └── index.ts ├── customTypes │ └── express-extend.d.ts ├── interfaces │ └── index.ts ├── middlewares │ └── authorizer.ts ├── models │ ├── albums │ │ ├── getById.ts │ │ ├── getCovers.ts │ │ ├── index.ts │ │ ├── likeAlbum.ts │ │ └── unlikeAlbum.ts │ ├── artists │ │ ├── getById.ts │ │ ├── getCovers.ts │ │ ├── index.ts │ │ ├── likeArtist.ts │ │ └── unlikeArtist.ts │ ├── dj-station │ │ ├── getCovers.ts │ │ └── index.ts │ ├── genres │ │ ├── getById.ts │ │ ├── getCovers.ts │ │ └── index.ts │ ├── library │ │ ├── albums.ts │ │ ├── artists.ts │ │ ├── index.ts │ │ ├── playlists.ts │ │ └── tracks.ts │ ├── magazines │ │ ├── getById.ts │ │ ├── getCovers.ts │ │ └── index.ts │ ├── news │ │ ├── getById.ts │ │ ├── getCovers.ts │ │ └── index.ts │ ├── playlists │ │ ├── getById.ts │ │ ├── getCovers.ts │ │ ├── index.ts │ │ ├── likePlaylist.ts │ │ └── unlikePlaylist.ts │ ├── tracks │ │ ├── getTrackCard.ts │ │ ├── getTrackForSearch.ts │ │ ├── index.ts │ │ ├── likeTrack.ts │ │ └── unlikeTrack.ts │ └── users │ │ └── index.ts ├── mongo.ts ├── package-lock.json ├── package.json ├── prisma │ ├── index.ts │ ├── migrations │ │ ├── 20201206122741-add-profile-to-user │ │ │ ├── README.md │ │ │ ├── schema.prisma │ │ │ └── steps.json │ │ └── migrate.lock │ └── schema.prisma ├── routes │ ├── albums │ │ └── index.ts │ ├── artists │ │ └── index.ts │ ├── auth │ │ └── index.ts │ ├── dj-stations │ │ └── index.ts │ ├── genres │ │ └── index.ts │ ├── index.ts │ ├── library │ │ └── index.ts │ ├── log │ │ └── index.ts │ ├── magazines │ │ └── index.ts │ ├── news │ │ └── index.ts │ ├── playlists │ │ └── index.ts │ ├── search │ │ └── index.ts │ ├── tracks │ │ └── index.ts │ └── users │ │ └── index.ts ├── tsconfig.json └── utils │ ├── cookies.ts │ ├── decodeJWT.ts │ ├── encodeJWT.ts │ └── makePrismaObtion.ts ├── frontend ├── .babelrc ├── .dockerignore ├── .eslintrc.json ├── .prettierrc ├── .storybook │ ├── main.js │ └── tsconfig.json ├── Dockerfile ├── components │ ├── Button │ │ ├── EllipsisButton │ │ │ ├── EllipsisModal.tsx │ │ │ └── index.tsx │ │ ├── HeartButton │ │ │ ├── index.tsx │ │ │ └── styled.ts │ │ ├── HoverEllipsisButton │ │ │ ├── index.stories.tsx │ │ │ └── index.tsx │ │ ├── HoverPlayButton │ │ │ ├── index.stories.tsx │ │ │ └── index.tsx │ │ └── SlidebarButton │ │ │ └── index.tsx │ ├── Card │ │ ├── index.stories.tsx │ │ ├── index.style.ts │ │ └── index.tsx │ ├── ChartSlider │ │ ├── ChartTrackCard │ │ │ ├── index.stories.tsx │ │ │ └── index.tsx │ │ ├── index.stories.tsx │ │ ├── index.tsx │ │ └── styled.ts │ ├── DescriptionHeader │ │ ├── index.tsx │ │ └── styled.ts │ ├── DetailPage │ │ ├── index.tsx │ │ └── styled.ts │ ├── GenreContainer │ │ ├── GenreCard │ │ │ ├── index.stories.tsx │ │ │ └── index.tsx │ │ ├── index.stories.tsx │ │ └── index.tsx │ ├── HotMagCard │ │ ├── HotMagCard.stories.tsx │ │ ├── index.tsx │ │ └── styled.ts │ ├── HoverImg │ │ ├── GeneralHoverCover │ │ │ └── index.tsx │ │ ├── TrackCardHoverCover │ │ │ └── index.tsx │ │ ├── img.style.ts │ │ ├── index.stories.tsx │ │ └── index.tsx │ ├── Img │ │ ├── Img.style.ts │ │ ├── index.stories.tsx │ │ └── index.tsx │ ├── Layout │ │ ├── Playbar │ │ │ ├── Overlay.tsx │ │ │ ├── PlaybarTrackCard │ │ │ │ ├── index.tsx │ │ │ │ └── styled.ts │ │ │ ├── index.tsx │ │ │ └── styled.ts │ │ ├── PlaylistCheckBar.styled.ts │ │ ├── PlaylistCheckBar.tsx │ │ ├── index.tsx │ │ └── styled.ts │ ├── LikedArtistCard │ │ └── index.tsx │ ├── MagLabel │ │ ├── MagLabel.interface.ts │ │ ├── MagLabel.stories.tsx │ │ ├── MagLabel.style.ts │ │ └── index.tsx │ ├── Modals │ │ ├── NewPlaylistModal │ │ │ └── index.tsx │ │ └── PlaylistSelector │ │ │ ├── ModalNewRow.tsx │ │ │ ├── ModalRow.tsx │ │ │ └── index.tsx │ ├── NavBar │ │ ├── LinkCardBlock.stories.tsx │ │ ├── LinkCardBlock │ │ │ ├── LinkCard │ │ │ │ ├── LinkCard.stories.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── styled.ts │ │ │ ├── LinkCardBlock.stories.tsx │ │ │ ├── index.tsx │ │ │ └── styled.ts │ │ ├── NavBarUser │ │ │ ├── NavBarUser.stories.tsx │ │ │ ├── index.tsx │ │ │ └── styled.ts │ │ ├── NavTopLogoSearch │ │ │ ├── NavTopLogoSearch.stories.tsx │ │ │ ├── index.tsx │ │ │ └── styled.ts │ │ ├── index.tsx │ │ └── styled.ts │ ├── SearchBar │ │ ├── index.tsx │ │ └── styled.ts │ ├── SearchSamples │ │ ├── SearchAlbumList │ │ │ ├── SearchAlbumCard │ │ │ │ ├── index.tsx │ │ │ │ └── styled.ts │ │ │ ├── index.tsx │ │ │ └── styled.ts │ │ ├── SearchArtistList │ │ │ ├── SearchArtistCard │ │ │ │ ├── index.tsx │ │ │ │ └── styled.ts │ │ │ ├── index.tsx │ │ │ └── styled.ts │ │ └── all │ │ │ ├── index.tsx │ │ │ └── styled.ts │ ├── Slidebar │ │ ├── func │ │ │ ├── calculatePixels.ts │ │ │ ├── onNextClicked.ts │ │ │ └── onPreviousClicked.ts │ │ ├── index.stories.tsx │ │ ├── index.tsx │ │ └── styled.ts │ └── Tracklist │ │ ├── TrackCard │ │ ├── index.tsx │ │ └── styled.ts │ │ ├── index.tsx │ │ └── styled.ts ├── constant │ └── icons.tsx ├── event │ ├── Readme.md │ ├── SequencedEventObserver.ts │ ├── collector.tsx │ ├── emitter.tsx │ ├── index.ts │ └── interface.ts ├── interfaces │ └── index.ts ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── about.tsx │ ├── albums │ │ └── [pid].tsx │ ├── artists │ │ └── [pid].tsx │ ├── auth │ │ ├── index.tsx │ │ ├── login.tsx │ │ └── signup.tsx │ ├── chart │ │ └── index.tsx │ ├── library │ │ ├── albums.tsx │ │ ├── artists.tsx │ │ ├── playlists.tsx │ │ └── tracks.tsx │ ├── magazines │ │ ├── [pid].tsx │ │ └── index.tsx │ ├── news │ │ └── [pid].tsx │ ├── playlists │ │ └── [pid].tsx │ ├── search │ │ ├── albums │ │ │ ├── index.tsx │ │ │ └── styled.ts │ │ ├── artists │ │ │ ├── index.tsx │ │ │ └── styled.ts │ │ ├── index.tsx │ │ ├── styled.ts │ │ └── tracks │ │ │ ├── index.tsx │ │ │ └── styled.ts │ └── today │ │ └── index.tsx ├── reduxModules │ ├── checkedTrack │ │ └── index.ts │ ├── index.ts │ └── playQueue │ │ └── index.ts ├── tsconfig.json └── utils │ ├── asyncAxios.ts │ ├── fetchLike.ts │ ├── findTokenFromCookie.ts │ ├── myAxios.ts │ ├── sendLog.ts │ └── svg.tsx └── iOS ├── MiniVibe ├── .swiftlint.yml ├── DiveEventCollector │ ├── .gitignore │ ├── LinuxMain.swift │ ├── Package.swift │ ├── README.md │ ├── Sources │ │ └── DiveEventCollector │ │ │ ├── AlertEventEngine.swift │ │ │ ├── BaseEvent.swift │ │ │ ├── Event.swift │ │ │ ├── EventEngineProtocols.swift │ │ │ ├── EventManager.swift │ │ │ ├── MockServerEngine.swift │ │ │ └── ReachabilityObserverDelegate.swift │ └── Tests │ │ ├── DiveEventCollectorTests │ │ ├── DiveEventCollectorTests.swift │ │ └── XCTestManifests.swift │ │ └── LinuxMain.swift ├── MiniVibe.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── MiniVibe.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved ├── MiniVibe │ ├── Common │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── Contents.json │ │ │ │ ├── Icon-App-1024x1024 – 3.png │ │ │ │ ├── Icon-App-20x20@1x – 3.png │ │ │ │ ├── Icon-App-20x20@2x – 3-1.png │ │ │ │ ├── Icon-App-20x20@2x – 3.png │ │ │ │ ├── Icon-App-20x20@3x – 3.png │ │ │ │ ├── Icon-App-29x29@1x – 3.png │ │ │ │ ├── Icon-App-29x29@2x – 3-1.png │ │ │ │ ├── Icon-App-29x29@2x – 3.png │ │ │ │ ├── Icon-App-29x29@3x – 3.png │ │ │ │ ├── Icon-App-40x40@1x – 3.png │ │ │ │ ├── Icon-App-40x40@2x – 3-1.png │ │ │ │ ├── Icon-App-40x40@2x – 3.png │ │ │ │ ├── Icon-App-40x40@3x – 3.png │ │ │ │ ├── Icon-App-60x60@2x – 3.png │ │ │ │ ├── Icon-App-60x60@3x – 3.png │ │ │ │ ├── Icon-App-76x76@1x – 3.png │ │ │ │ ├── Icon-App-76x76@2x – 3.png │ │ │ │ └── Icon-App-83.5x83.5@2x – 3.png │ │ │ ├── Contents.json │ │ │ └── images │ │ │ │ ├── Blueming.imageset │ │ │ │ ├── Blueming.jpg │ │ │ │ └── Contents.json │ │ │ │ ├── Contents.json │ │ │ │ ├── Dynamite.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── Dynamite.jpg │ │ │ │ ├── FeelGood.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── FeelGood.jpg │ │ │ │ ├── Lovesick Girls.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── Lovesick Girls.jpg │ │ │ │ ├── appIcon.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── appIcon.png │ │ │ │ ├── dj1.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── dj1.png │ │ │ │ ├── dj2.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── dj2.png │ │ │ │ ├── dj3.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── dj3.png │ │ │ │ ├── dj4.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── dj4.png │ │ │ │ ├── dj5.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── dj5.png │ │ │ │ ├── dj6.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── dj6.png │ │ │ │ ├── dj7.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── dj7.png │ │ │ │ ├── dj8.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── dj8.png │ │ │ │ ├── dj9.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── dj9.png │ │ │ │ ├── favorite1.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── favorite1.png │ │ │ │ ├── favorite2.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── favorite2.png │ │ │ │ ├── favorite3.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── favorite3.png │ │ │ │ ├── favorite4.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── favorite4.jpg │ │ │ │ ├── favorite5.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── favorite5.jpg │ │ │ │ ├── logo.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── logo.png │ │ │ │ ├── magazine1.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── magazine1.png │ │ │ │ ├── magazine2.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── magazine2.png │ │ │ │ ├── magazine3.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── magazine3.png │ │ │ │ ├── magazine4.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── magazine4.png │ │ │ │ ├── magazine5.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── magazine5.png │ │ │ │ ├── recommend1.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── recommend1.png │ │ │ │ ├── recommend2.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── recommend2.png │ │ │ │ ├── recommend3.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── recommend3.png │ │ │ │ ├── recommend4.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── recommend4.png │ │ │ │ └── recommend5.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── recommend5.png │ │ ├── CustomView │ │ │ ├── Accessory │ │ │ │ ├── DeleteAccessory.swift │ │ │ │ ├── DownloadAccessory.swift │ │ │ │ └── EllipsisAccessory.swift │ │ │ ├── BasicRowCellView.swift │ │ │ ├── MemorySafeNavigationLink.swift │ │ │ └── SwappableRowView.swift │ │ ├── Extension │ │ │ ├── Data+PrettifyJSONString.swift │ │ │ ├── Date+StringForWeb.swift │ │ │ ├── Image+accesoryModifier.swift │ │ │ ├── Rectangle+Modifier.swift │ │ │ └── Sequence+indexed.swift │ │ ├── Image │ │ │ ├── ActivityIndicatorView.swift │ │ │ ├── ImageCache.swift │ │ │ ├── SwappableImageLoaderAndCache.swift │ │ │ ├── SwappableImageWithURL.swift │ │ │ ├── URLImage.swift │ │ │ └── URLImageLoader.swift │ │ ├── Network │ │ │ ├── CacheService.swift │ │ │ ├── MiniVibeType.swift │ │ │ ├── NetworkManager.swift │ │ │ ├── NetworkService.swift │ │ │ ├── RequestBuilder.swift │ │ │ └── URLBuilder.swift │ │ ├── Router │ │ │ └── RouterProtocol.swift │ │ ├── Test │ │ │ ├── DeallocPrinter.swift │ │ │ └── TestData.swift │ │ └── ViewModifier │ │ │ ├── ButtonStyle.swift │ │ │ ├── FontStyle.swift │ │ │ └── NavigationBarStyle.swift │ ├── CoreData │ │ ├── API │ │ │ ├── CoreDataAPIManager.swift │ │ │ ├── CoreEventAPI.swift │ │ │ └── CoreTrackAPI.swift │ │ ├── MappingModelV1ToV2.xcmappingmodel │ │ │ └── xcmapping.xml │ │ ├── MappingModelV2ToV3.xcmappingmodel │ │ │ └── xcmapping.xml │ │ ├── MiniVibe.xcdatamodeld │ │ │ ├── .xccurrentversion │ │ │ ├── MiniVibe v2.xcdatamodel │ │ │ │ └── contents │ │ │ ├── MiniVibe v3.xcdatamodel │ │ │ │ └── contents │ │ │ └── MiniVibe.xcdatamodel │ │ │ │ └── contents │ │ └── Persistence.swift │ ├── Info.plist │ ├── MiniVibeApp.swift │ ├── MiniVibeEventCollector │ │ ├── Engines │ │ │ ├── BackupEventEngine.swift │ │ │ └── MongoDBEventEngine.swift │ │ ├── Events │ │ │ ├── ButtonEvent.swift │ │ │ ├── PlayerEvent.swift │ │ │ └── ScreenEvent.swift │ │ └── Views │ │ │ └── LoggableButton.swift │ ├── Models │ │ ├── Album.swift │ │ ├── Artist.swift │ │ ├── DJStation.swift │ │ ├── Magazine.swift │ │ ├── Playlist.swift │ │ ├── Search.swift │ │ ├── Track.swift │ │ └── User.swift │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ └── Scenes │ │ ├── Chart │ │ └── ChartView.swift │ │ ├── DJStation │ │ ├── DJStationListView.swift │ │ └── DJStationListViewModel.swift │ │ ├── Error │ │ └── ErrorView.swift │ │ ├── Library │ │ ├── LibraryView.swift │ │ └── LibraryViewModel.swift │ │ ├── Magazine │ │ ├── MagazineView.swift │ │ └── MagazineViewModel.swift │ │ ├── NowPlaying │ │ ├── BlurView.swift │ │ └── NowPlayingView.swift │ │ ├── Player │ │ ├── PlayerView.swift │ │ ├── PlayerViewModel.swift │ │ └── SubViews │ │ │ ├── PlayerControlView.swift │ │ │ ├── PlayerHeaderView.swift │ │ │ ├── PlayerInfoView.swift │ │ │ ├── SwipableImageView.swift │ │ │ └── ToggleableImage.swift │ │ ├── Playlist │ │ ├── PlaylistCellView.swift │ │ ├── PlaylistListView.swift │ │ ├── PlaylistRouter.swift │ │ ├── PlaylistView.swift │ │ └── PlaylistViewModel.swift │ │ ├── Search │ │ ├── SearchView.swift │ │ ├── SearchViewModel.swift │ │ └── SubViews │ │ │ ├── GenreCellView.swift │ │ │ ├── GenreListView.swift │ │ │ ├── RectangleCellInfoView.swift │ │ │ ├── RectangleCellView.swift │ │ │ ├── RectangleListView.swift │ │ │ ├── SearchAfterCategoryView.swift │ │ │ ├── SearchAfterView.swift │ │ │ ├── SearchBarView.swift │ │ │ └── SearchBeforeView.swift │ │ ├── TabBar │ │ ├── CustomTabView.swift │ │ ├── CustomTabViewContent.swift │ │ ├── TabBarView.swift │ │ └── TabIconView.swift │ │ ├── Thumbnail │ │ ├── ThumbnailCellView.swift │ │ ├── ThumbnailListView.swift │ │ ├── ThumbnailListViewModel.swift │ │ └── ThumbnailRouter.swift │ │ ├── Today │ │ ├── SubViews │ │ │ ├── Category.swift │ │ │ ├── CategoryCellView.swift │ │ │ ├── CategoryHeaderView.swift │ │ │ ├── CategoryItem.swift │ │ │ ├── CategoryRouter.swift │ │ │ └── CategoryView.swift │ │ ├── TodayRouter.swift │ │ ├── TodayView.swift │ │ └── TodayViewModel.swift │ │ └── TrackList │ │ ├── SubViews │ │ ├── TrackCellView.swift │ │ ├── TrackCellViewModel.swift │ │ ├── TrackHorizontalListView.swift │ │ ├── TrackListButtonView.swift │ │ └── TrackListHeaderView.swift │ │ └── TrackListView.swift ├── MiniVibeTests │ ├── Info.plist │ └── MiniVibeTests.swift ├── MiniVibeUITests │ ├── Info.plist │ ├── MiniVibeUITests.swift │ ├── XCUIApplication+Alert.swift │ └── XCUIElement+Scroll.swift ├── Podfile └── Podfile.lock └── README.md /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 제목: [iOS] 제목입니다. 3 | 4 | >한줄 요약 5 | 6 | ## 구현내용 7 | 8 | ### 화면(optional) 9 | 10 | ### 학습 내용(optional) 11 | 어떤 기술써서 어떻게 했다. 에 대한 기술 설명 12 | 13 | ## 논의사항 14 | ex) 추가적으로 주석이나, 코딩 컨벤션 같은 걸 명확히 하지 않아 함수명 짓는데 고민되는 것 같아요 15 | ```func setNavigationBar()``` 과 같은 이름의 임의로(?) 사용하였는데 같이 얘기 나눠 보고 싶네요❗️ 16 | -------------------------------------------------------------------------------- /backend/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /backend/.eslintignore: -------------------------------------------------------------------------------- 1 | /**/*.d.ts -------------------------------------------------------------------------------- /backend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "parser": "babel-eslint", 8 | "extends": [ 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "airbnb", 12 | "plugin:prettier/recommended" 13 | ], 14 | "settings": { 15 | "react": { 16 | "version": "detect" 17 | }, 18 | "import/resolver": { 19 | "node": { 20 | "extensions": [".js", ".ts", ".d.ts"] 21 | } 22 | } 23 | }, 24 | "parserOptions": { 25 | "ecmaFeatures": { 26 | "jsx": true 27 | }, 28 | "ecmaVersion": 2018, 29 | "sourceType": "module" 30 | }, 31 | "plugins": ["prettier"], 32 | "rules": { 33 | "no-nested-ternary": 0, 34 | "prettier/prettier": [ 35 | "error", 36 | { 37 | "endOfLine": "auto" 38 | } 39 | ], 40 | "import/extensions": [ 41 | "error", 42 | "ignorePackages", 43 | { 44 | "js": "never", 45 | "mjs": "never", 46 | "jsx": "never", 47 | "ts": "never", 48 | "tsx": "never" 49 | } 50 | ] 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12.18.3 2 | USER root 3 | 4 | MAINTAINER marullo Cho 5 | 6 | RUN mkdir -p /expressServer 7 | WORKDIR /expressServer 8 | ADD . /expressServer 9 | 10 | RUN npm install 11 | RUN npx prisma generate 12 | 13 | EXPOSE 4000 14 | 15 | CMD "npm" "start" -------------------------------------------------------------------------------- /backend/app.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import dotenv from "dotenv"; 3 | import express from "express"; 4 | import logger from "morgan"; 5 | import cookieParser from "cookie-parser"; 6 | import cors from "cors"; 7 | import bodyParser from "body-parser"; 8 | import apiRouter from "./routes"; 9 | import { connect } from "./mongo"; 10 | import authorizer from "./middlewares/authorizer"; 11 | 12 | if (process.env.NODE_ENV === "production") { 13 | dotenv.config({ path: ".env.production" }); 14 | } else { 15 | dotenv.config({ path: ".env.development" }); 16 | } 17 | 18 | const corsOptions = { 19 | origin: "http://118.67.135.69:3000", 20 | optionsSuccessStatus: 200, 21 | }; 22 | 23 | const app = express(); 24 | app.use(express.json()); 25 | app.use(express.urlencoded({ extended: false })); 26 | app.use(cookieParser()); 27 | app.use(logger("short")); 28 | app.use(cors()); 29 | app.use(bodyParser.urlencoded({ extended: true })); 30 | 31 | connect(); 32 | 33 | app.use("/api", authorizer, apiRouter); 34 | 35 | app.listen(process.env.PORT || 4000, () => { 36 | console.log(`Server Start on Stage: ${process.env.NODE_ENV}`); 37 | }); 38 | -------------------------------------------------------------------------------- /backend/controllers/auth/index.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import signup from "./local/signup"; 3 | import login from "./local/login"; 4 | import naverOAuth from "./naver/callbackProcess"; 5 | import naverRedir from "./naver/redirection"; 6 | 7 | interface Auth { 8 | signup: (req: Request, res: Response) => Promise; 9 | login: (req: Request, res: Response) => Promise; 10 | naverCallback: (req: Request, res: Response) => Promise; 11 | naverRedirection: (req: Request, res: Response) => void; 12 | } 13 | 14 | const authController: Auth = { 15 | signup, 16 | login, 17 | naverCallback: naverOAuth, 18 | naverRedirection: naverRedir, 19 | }; 20 | 21 | export default authController; 22 | -------------------------------------------------------------------------------- /backend/controllers/auth/local/login.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import crypto from "crypto"; 3 | import { getUserInfo } from "../../../models/users"; 4 | import encodeJWT from "../../../utils/encodeJWT"; 5 | 6 | const login = async (req: Request, res: Response): Promise => { 7 | const { username, password } = req.body; 8 | const loginResult = await getUserInfo({ 9 | username, 10 | password: crypto.createHash("sha512").update(password).digest("hex"), 11 | }); 12 | const jwt = encodeJWT(loginResult); 13 | if (!loginResult) 14 | return res.status(400).send({ 15 | result: 16 | "해당하는 아이디, 비밀번호에 해당하는 유저 정보가 존재하지 않습니다.", 17 | }); 18 | return res.status(200).send({ result: "success", token: jwt }); 19 | }; 20 | 21 | export default login; 22 | -------------------------------------------------------------------------------- /backend/controllers/auth/local/signup.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import crypto from "crypto"; 3 | import { postUserInfo } from "../../../models/users"; 4 | 5 | const signup = async (req: Request, res: Response): Promise => { 6 | try { 7 | const { username, password1, password2 } = req.body; 8 | if (password1 !== password2) { 9 | return res 10 | .status(400) 11 | .send({ message: "비밀번호와 비밀번호 확인이 동일하지 않습니다." }); 12 | } 13 | await postUserInfo({ 14 | username, 15 | password: crypto.createHash("sha512").update(password1).digest("hex"), 16 | }); 17 | 18 | return res.status(200).send({ result: "success" }); 19 | } catch (err) { 20 | return res.send(400).send({ err }); 21 | } 22 | }; 23 | 24 | export default signup; 25 | -------------------------------------------------------------------------------- /backend/controllers/auth/naver/redirection.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | 3 | const naverRedirect = (req: Request, res: Response): void => { 4 | const naverUrl: string = process.env.NAVER_LOGIN_URL || "/"; 5 | res.redirect(naverUrl); 6 | }; 7 | 8 | export default naverRedirect; 9 | -------------------------------------------------------------------------------- /backend/controllers/dj-stations/index.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import { getDjStationCovers } from "../../models/dj-station"; 3 | 4 | interface Controller { 5 | getAll(req: Request, res: Response): Promise; 6 | } 7 | 8 | const getAll = async (req: Request, res: Response): Promise => { 9 | try { 10 | const result = await getDjStationCovers({}); 11 | res.status(200).json({ DJStations: result }); 12 | } catch (err) { 13 | res.status(500).json({ statusCode: 500, message: err.message }); 14 | } 15 | }; 16 | 17 | const controller: Controller = { getAll }; 18 | export default controller; 19 | -------------------------------------------------------------------------------- /backend/controllers/genres/index.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import { getGenreCovers, getGenreById } from "../../models/genres"; 3 | 4 | interface Controller { 5 | getAll(req: Request, res: Response): Promise; 6 | getGenre(req: Request, res: Response): Promise; 7 | } 8 | 9 | const getAll = async (req: Request, res: Response): Promise => { 10 | try { 11 | const result = await getGenreCovers(); 12 | res.status(200).json({ Genres: result }); 13 | } catch (err) { 14 | res.status(500).json({ statusCode: 500, message: err.message }); 15 | } 16 | }; 17 | 18 | const getGenre = async (req: Request, res: Response): Promise => { 19 | const { id } = req.params; 20 | 21 | try { 22 | const result = await getGenreById(+id); 23 | if (!result) throw new Error("empty data"); 24 | res.status(200).json({ Genres: result }); 25 | } catch (err) { 26 | if (err === "empty data") { 27 | res.status(400).json({ statusCode: 400, message: "Bad Request" }); 28 | } else { 29 | res.status(500).json({ statusCode: 500, message: err.message }); 30 | } 31 | } 32 | }; 33 | 34 | const controller: Controller = { getAll, getGenre }; 35 | export default controller; 36 | -------------------------------------------------------------------------------- /backend/controllers/log/iOS.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import { db } from "../../mongo"; 3 | 4 | const getiOSLog = async (req: Request, res: Response): Promise => { 5 | db.iOS.find({}).toArray((error, result) => { 6 | if (error) throw error; 7 | 8 | res.send(result); 9 | }); 10 | }; 11 | 12 | const postiOSLog = async (req: Request, res: Response): Promise => { 13 | const insertData = { 14 | ...req.body, 15 | "user-agent": req.headers["user-agent"], 16 | loggedIn: !!req.user, 17 | userId: req.user ? req.user.id : null, 18 | username: req.user ? req.user.username : null, 19 | }; 20 | db.iOS.insertOne(insertData, (err, r) => { 21 | return res.send(r); 22 | }); 23 | }; 24 | 25 | export { getiOSLog, postiOSLog }; 26 | -------------------------------------------------------------------------------- /backend/controllers/log/index.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import { getWebLog, postWebLog } from "./web"; 3 | import { getiOSLog, postiOSLog } from "./iOS"; 4 | 5 | interface Controller { 6 | getWebLog(req: Request, res: Response): Promise; 7 | getiOSLog(req: Request, res: Response): Promise; 8 | postWebLog(req: Request, res: Response): Promise; 9 | postiOSLog(req: Request, res: Response): Promise; 10 | } 11 | 12 | const controller: Controller = { 13 | getWebLog, 14 | getiOSLog, 15 | postWebLog, 16 | postiOSLog, 17 | }; 18 | 19 | export default controller; 20 | -------------------------------------------------------------------------------- /backend/controllers/log/web.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import { db } from "../../mongo"; 3 | 4 | const getWebLog = async (req: Request, res: Response): Promise => { 5 | db.web.find({}).toArray((error, result) => { 6 | if (error) throw error; 7 | 8 | res.send(result); 9 | }); 10 | }; 11 | 12 | const postWebLog = async (req: Request, res: Response): Promise => { 13 | const insertData = { 14 | ...req.body, 15 | "user-agent": req.headers["user-agent"], 16 | loggedIn: !!req.user, 17 | userId: req.user ? req.user.id : null, 18 | username: req.user ? req.user.username : null, 19 | }; 20 | db.web.insertOne(insertData, (err, r) => { 21 | return res.send(r); 22 | }); 23 | }; 24 | 25 | export { getWebLog, postWebLog }; 26 | -------------------------------------------------------------------------------- /backend/controllers/news/index.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import { getNewsCovers, getNewsById } from "../../models/news"; 3 | import { makeOption } from "../../utils/makePrismaObtion"; 4 | 5 | interface Controller { 6 | getAll(req: Request, res: Response): Promise; 7 | getNews(req: Request, res: Response): Promise; 8 | } 9 | 10 | const getAll = async (req: Request, res: Response): Promise => { 11 | try { 12 | const optObj = makeOption(req.query); 13 | const result = await getNewsCovers(optObj); 14 | res.status(200).json({ News: result }); 15 | } catch (err) { 16 | res.status(500).json({ statusCode: 500, message: err.message }); 17 | } 18 | }; 19 | 20 | const getNews = async (req: Request, res: Response): Promise => { 21 | const { id } = req.params; 22 | 23 | try { 24 | const result = await getNewsById(+id, req.user); 25 | if (!result) throw new Error("empty data"); 26 | res.status(200).json({ News: result }); 27 | } catch (err) { 28 | if (err === "empty data") { 29 | res.status(400).json({ statusCode: 400, message: "Bad Request" }); 30 | } else { 31 | res.status(500).json({ statusCode: 500, message: err.message }); 32 | } 33 | } 34 | }; 35 | 36 | const controller: Controller = { getAll, getNews }; 37 | export default controller; 38 | -------------------------------------------------------------------------------- /backend/controllers/search/getAlbums.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import { makeSearchOption } from "../../utils/makePrismaObtion"; 3 | import { getAlbumCovers } from "../../models/albums"; 4 | 5 | const getAlbums = async (req: Request, res: Response): Promise => { 6 | const albumFilter = makeSearchOption(req.query, "albumName"); 7 | 8 | try { 9 | const result = await getAlbumCovers(albumFilter); 10 | 11 | res.status(200).json(result); 12 | } catch (err) { 13 | res.status(500).json({ statusCode: 500, message: err.message }); 14 | } 15 | }; 16 | 17 | export default getAlbums; 18 | -------------------------------------------------------------------------------- /backend/controllers/search/getAll.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import { makeSearchOption } from "../../utils/makePrismaObtion"; 3 | import { getAlbumCovers } from "../../models/albums"; 4 | import { getArtistCovers } from "../../models/artists"; 5 | import { getTrackForSearch } from "../../models/tracks"; 6 | 7 | const getAll = async (req: Request, res: Response): Promise => { 8 | const albumFilter = makeSearchOption(req.query, "albumName"); 9 | const trackFilter = makeSearchOption(req.query, "trackName"); 10 | const artistFilter = makeSearchOption(req.query, "artistName"); 11 | 12 | try { 13 | const result: any = {}; 14 | 15 | // TODO : PROMISE ALL 16 | result.Albums = await getAlbumCovers(albumFilter); 17 | result.Tracks = await getTrackForSearch(trackFilter, req.user); 18 | result.Artists = await getArtistCovers(artistFilter); 19 | 20 | res.status(200).json(result); 21 | } catch (err) { 22 | res.status(500).json({ statusCode: 500, message: err.message }); 23 | } 24 | }; 25 | 26 | export default getAll; 27 | -------------------------------------------------------------------------------- /backend/controllers/search/getArtists.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import { makeSearchOption } from "../../utils/makePrismaObtion"; 3 | import { getArtistCovers } from "../../models/artists"; 4 | 5 | const getArtists = async (req: Request, res: Response): Promise => { 6 | const artistFilter = makeSearchOption(req.query, "artistName"); 7 | 8 | try { 9 | const result = await getArtistCovers(artistFilter); 10 | 11 | res.status(200).json(result); 12 | } catch (err) { 13 | res.status(500).json({ statusCode: 500, message: err.message }); 14 | } 15 | }; 16 | 17 | export default getArtists; 18 | -------------------------------------------------------------------------------- /backend/controllers/search/getTracks.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import { makeSearchOption } from "../../utils/makePrismaObtion"; 3 | import { getTrackForSearch } from "../../models/tracks"; 4 | 5 | const getTracks = async (req: Request, res: Response): Promise => { 6 | const trackFilter = makeSearchOption(req.query, "trackName"); 7 | 8 | try { 9 | const result = await getTrackForSearch(trackFilter, req.user); 10 | 11 | res.status(200).json(result); 12 | } catch (err) { 13 | res.status(500).json({ statusCode: 500, message: err.message }); 14 | } 15 | }; 16 | 17 | export default getTracks; 18 | -------------------------------------------------------------------------------- /backend/controllers/search/index.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import getAll from "./getAll"; 3 | import getTracks from "./getTracks"; 4 | import getAlbums from "./getAlbums"; 5 | import getArtists from "./getArtists"; 6 | 7 | interface Controller { 8 | getAll(req: Request, res: Response): Promise; 9 | getTracks(req: Request, res: Response): Promise; 10 | getAlbums(req: Request, res: Response): Promise; 11 | getArtists(req: Request, res: Response): Promise; 12 | } 13 | 14 | const controller: Controller = { 15 | getAll, 16 | getTracks, 17 | getAlbums, 18 | getArtists, 19 | }; 20 | export default controller; 21 | -------------------------------------------------------------------------------- /backend/controllers/tracks/index.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import { likeTrack, unlikeTrack } from "../../models/tracks"; 3 | 4 | interface Controller { 5 | like(req: Request, res: Response): Promise; 6 | unlike(req: Request, res: Response): Promise; 7 | } 8 | 9 | const like = async (req: Request, res: Response): Promise => { 10 | try { 11 | const { trackId } = req.body; 12 | const result = await likeTrack(trackId, req.user); 13 | res.status(200).json({ Albums: result }); 14 | } catch (err) { 15 | res.status(500).json({ statusCode: 500, message: err.message }); 16 | } 17 | }; 18 | 19 | const unlike = async (req: Request, res: Response): Promise => { 20 | try { 21 | const { trackId } = req.body; 22 | const result = await unlikeTrack(trackId, req.user); 23 | res.status(200).json({ Albums: result }); 24 | } catch (err) { 25 | res.status(500).json({ statusCode: 500, message: err.message }); 26 | } 27 | }; 28 | 29 | const controller: Controller = { like, unlike }; 30 | export default controller; 31 | -------------------------------------------------------------------------------- /backend/controllers/users/index.ts: -------------------------------------------------------------------------------- 1 | import getUserProfile from "./userProfile"; 2 | import getLikedItem from "./userLikedItems"; 3 | 4 | interface User { 5 | getUserProfile: any; 6 | getLikedItem: any; 7 | } 8 | 9 | const userController: User = { 10 | getUserProfile, 11 | getLikedItem, 12 | }; 13 | 14 | export default userController; 15 | -------------------------------------------------------------------------------- /backend/controllers/users/userProfile/index.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import { getUserInfoWithID } from "../../../models/users"; 3 | 4 | const getUserProfile = async (req: Request, res: Response): Promise => { 5 | try { 6 | if (!req.user) throw new Error("Unauthorized"); 7 | 8 | const userProfile = await getUserInfoWithID(req.user.id); 9 | res.status(200).send({ userProfile }); 10 | } catch (err) { 11 | res.status(401).send({ message: "Unauthroized" }); 12 | } 13 | }; 14 | 15 | export default getUserProfile; 16 | -------------------------------------------------------------------------------- /backend/customTypes/express-extend.d.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express'; 2 | 3 | declare module 'express' { 4 | export interface Request { 5 | user?: { 6 | id: number; 7 | username: string; 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /backend/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | interface Artist { 2 | id: number; 3 | artistName: string; 4 | cover: string; 5 | } 6 | 7 | interface Album { 8 | id: number; 9 | albumName: string; 10 | description?: string; 11 | cover: string; 12 | artistId: number; 13 | } 14 | 15 | interface Track { 16 | id: number; 17 | trackName: string; 18 | albumTrackNumber: number; 19 | albumId: number; 20 | Albums: Album; 21 | Artists: Artist[]; 22 | Liked: boolean; 23 | } 24 | 25 | interface Playlist { 26 | id: number; 27 | playlistName: string; 28 | description?: string; 29 | cover?: string; 30 | author: number; 31 | } 32 | 33 | interface Magazine { 34 | id: number; 35 | magazineName: string; 36 | magazineType: string; 37 | description: string; 38 | createdAt: number; 39 | playlistId: number; 40 | } 41 | 42 | interface News { 43 | id: number; 44 | newsName: string; 45 | type?: string; 46 | description?: string; 47 | playlistId?: number; 48 | } 49 | 50 | export type { Track, Artist, Album, Playlist, Magazine, News }; 51 | -------------------------------------------------------------------------------- /backend/middlewares/authorizer.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | import decodeJWT from "../utils/decodeJWT"; 3 | 4 | const authorizer = ( 5 | req: Request, 6 | res: Response, 7 | next: NextFunction 8 | ): Response | void => { 9 | try { 10 | const mainPath = req.path.split("/")[1]; 11 | const { authorization } = req.headers; 12 | 13 | if (authorization) { 14 | const token = authorization.split(" ")[1]; 15 | const userInfo = decodeJWT(token); 16 | req.user = userInfo; 17 | } else if (mainPath === "user" || mainPath === "library") { 18 | throw new Error("Unauthorized"); 19 | } 20 | return next(); 21 | } catch (err) { 22 | return res.status(401).send(err); 23 | } 24 | }; 25 | 26 | export default authorizer; 27 | -------------------------------------------------------------------------------- /backend/models/albums/getCovers.ts: -------------------------------------------------------------------------------- 1 | import prisma from "../../prisma"; 2 | 3 | const getAlbumCovers = async (optObj: any): Promise => { 4 | // eslint-disable-next-line no-param-reassign 5 | optObj.include = { 6 | Artists: { 7 | select: { artistName: true }, 8 | }, 9 | }; 10 | const result = await prisma.albums.findMany(optObj); 11 | return result; 12 | }; 13 | 14 | export default getAlbumCovers; 15 | -------------------------------------------------------------------------------- /backend/models/albums/index.ts: -------------------------------------------------------------------------------- 1 | import getAlbumById from "./getById"; 2 | import getAlbumCovers from "./getCovers"; 3 | import likeAlbum from "./likeAlbum"; 4 | import unlikeAlbum from "./unlikeAlbum"; 5 | 6 | export { getAlbumById, getAlbumCovers, likeAlbum, unlikeAlbum }; 7 | -------------------------------------------------------------------------------- /backend/models/albums/likeAlbum.ts: -------------------------------------------------------------------------------- 1 | import prisma from "../../prisma"; 2 | 3 | const likeAlbum = async ( 4 | albumId: number, 5 | user: any | undefined 6 | ): Promise => { 7 | const album: any = await prisma.users_Like_Albums.findFirst({ 8 | where: { albumId }, 9 | }); 10 | if (album && !user) return null; 11 | 12 | const result = await prisma.users_Like_Albums.create({ 13 | data: { 14 | Albums: { 15 | connect: { 16 | id: albumId, 17 | }, 18 | }, 19 | Users: { 20 | connect: { 21 | id: user.id, 22 | }, 23 | }, 24 | }, 25 | }); 26 | 27 | return result; 28 | }; 29 | 30 | export default likeAlbum; 31 | -------------------------------------------------------------------------------- /backend/models/albums/unlikeAlbum.ts: -------------------------------------------------------------------------------- 1 | import prisma from "../../prisma"; 2 | 3 | const unlikeAlbum = async ( 4 | albumId: number, 5 | user: any | undefined 6 | ): Promise => { 7 | const album: any = await prisma.users_Like_Albums.findFirst({ 8 | where: { albumId }, 9 | }); 10 | if (!album && !user) return null; 11 | 12 | const result = await prisma.users_Like_Albums.delete({ 13 | where: { 14 | id: album.id, 15 | }, 16 | }); 17 | 18 | return result; 19 | }; 20 | 21 | export default unlikeAlbum; 22 | -------------------------------------------------------------------------------- /backend/models/artists/getCovers.ts: -------------------------------------------------------------------------------- 1 | import prisma from "../../prisma"; 2 | 3 | const getArtistCovers = async (optObj: any): Promise => { 4 | // eslint-disable-next-line no-param-reassign 5 | optObj.include = { 6 | Albums: { 7 | select: { albumName: true }, 8 | }, 9 | }; 10 | const result = await prisma.artists.findMany(optObj); 11 | return result; 12 | }; 13 | 14 | export default getArtistCovers; 15 | -------------------------------------------------------------------------------- /backend/models/artists/index.ts: -------------------------------------------------------------------------------- 1 | import getArtistById from "./getById"; 2 | import getArtistCovers from "./getCovers"; 3 | import likeArtist from "./likeArtist"; 4 | import unlikeArtist from "./unlikeArtist"; 5 | 6 | export { getArtistById, getArtistCovers, likeArtist, unlikeArtist }; 7 | -------------------------------------------------------------------------------- /backend/models/artists/likeArtist.ts: -------------------------------------------------------------------------------- 1 | import prisma from "../../prisma"; 2 | 3 | const likeArtist = async ( 4 | artistId: number, 5 | user: any | undefined 6 | ): Promise => { 7 | const artist: any = await prisma.users_Like_Artists.findFirst({ 8 | where: { artistId }, 9 | }); 10 | if (artist && !user) return null; 11 | 12 | const result = await prisma.users_Like_Artists.create({ 13 | data: { 14 | Artists: { 15 | connect: { 16 | id: artistId, 17 | }, 18 | }, 19 | Users: { 20 | connect: { 21 | id: user.id, 22 | }, 23 | }, 24 | }, 25 | }); 26 | 27 | return result; 28 | }; 29 | 30 | export default likeArtist; 31 | -------------------------------------------------------------------------------- /backend/models/artists/unlikeArtist.ts: -------------------------------------------------------------------------------- 1 | import prisma from "../../prisma"; 2 | 3 | const unlikeArtist = async ( 4 | artistId: number, 5 | user: any | undefined 6 | ): Promise => { 7 | const artist: any = await prisma.users_Like_Artists.findFirst({ 8 | where: { artistId }, 9 | }); 10 | if (!artist && !user) return null; 11 | 12 | const result = await prisma.users_Like_Artists.delete({ 13 | where: { 14 | id: artist.id, 15 | }, 16 | }); 17 | 18 | return result; 19 | }; 20 | 21 | export default unlikeArtist; 22 | -------------------------------------------------------------------------------- /backend/models/dj-station/getCovers.ts: -------------------------------------------------------------------------------- 1 | import prisma from "../../prisma"; 2 | 3 | const getDjStationCovers = async (optObj: Object): Promise => { 4 | const result = await prisma.dJStations.findMany(optObj); 5 | return result; 6 | }; 7 | 8 | export default getDjStationCovers; 9 | -------------------------------------------------------------------------------- /backend/models/dj-station/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | import getDjStationCovers from "./getCovers"; 3 | 4 | export { getDjStationCovers }; 5 | -------------------------------------------------------------------------------- /backend/models/genres/getById.ts: -------------------------------------------------------------------------------- 1 | import prisma from "../../prisma"; 2 | import { getTrackCard } from "../tracks"; 3 | 4 | const getGenreById = async (id: number): Promise => { 5 | const genre: any = await prisma.genres.findUnique({ where: { id } }); 6 | if (!genre) return null; 7 | 8 | const trackIds: any = await prisma.tracks_Genres.findMany({ 9 | where: { genreId: id }, 10 | }); 11 | const albumIds: any = await prisma.albums_Genres.findMany({ 12 | where: { genreId: id }, 13 | }); 14 | const artistIds: any = await prisma.artists_Genres.findMany({ 15 | where: { genreId: id }, 16 | }); 17 | 18 | // TODO: REFACTOR PROMISE.ALL, MAKE MORE FASTER 19 | genre.Tracks = await Promise.all( 20 | trackIds.map((elem: any) => getTrackCard(elem.trackId)) 21 | ); 22 | genre.Albums = await Promise.all( 23 | albumIds.map((elem: any) => 24 | prisma.albums.findUnique({ where: { id: elem.albumId } }) 25 | ) 26 | ); 27 | genre.Artists = await Promise.all( 28 | artistIds.map((elem: any) => 29 | prisma.artists.findUnique({ where: { id: elem.artistId } }) 30 | ) 31 | ); 32 | 33 | return genre; 34 | }; 35 | 36 | export default getGenreById; 37 | -------------------------------------------------------------------------------- /backend/models/genres/getCovers.ts: -------------------------------------------------------------------------------- 1 | import prisma from "../../prisma"; 2 | 3 | const getGenreCovers = async (): Promise => { 4 | const result = await prisma.genres.findMany(); 5 | return result; 6 | }; 7 | 8 | export default getGenreCovers; 9 | -------------------------------------------------------------------------------- /backend/models/genres/index.ts: -------------------------------------------------------------------------------- 1 | import getGenreById from "./getById"; 2 | import getGenreCovers from "./getCovers"; 3 | 4 | export { getGenreById, getGenreCovers }; 5 | -------------------------------------------------------------------------------- /backend/models/library/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getUserLikeTracks, 3 | postUserLikeTracks, 4 | deleteUserLikeTracks, 5 | } from "./tracks"; 6 | import { 7 | getUserLikeAlbums, 8 | postUserLikeAlbums, 9 | deleteUserLikeAlbums, 10 | } from "./albums"; 11 | import { 12 | getUserLikeArtists, 13 | postUserLikeArtists, 14 | deleteUserLikeArtists, 15 | } from "./artists"; 16 | import { 17 | getUserLikePlaylists, 18 | postUserLikePlaylists, 19 | deleteUserLikePlaylists, 20 | } from "./playlists"; 21 | 22 | export { 23 | getUserLikeAlbums, 24 | getUserLikeTracks, 25 | getUserLikeArtists, 26 | getUserLikePlaylists, 27 | postUserLikeAlbums, 28 | postUserLikeArtists, 29 | postUserLikePlaylists, 30 | postUserLikeTracks, 31 | deleteUserLikeAlbums, 32 | deleteUserLikeArtists, 33 | deleteUserLikePlaylists, 34 | deleteUserLikeTracks, 35 | }; 36 | -------------------------------------------------------------------------------- /backend/models/magazines/getCovers.ts: -------------------------------------------------------------------------------- 1 | import prisma from "../../prisma"; 2 | 3 | const getMagazineCovers = async (optObj: Object): Promise => { 4 | const result = await prisma.magazines.findMany(optObj); 5 | return result; 6 | }; 7 | 8 | export default getMagazineCovers; 9 | -------------------------------------------------------------------------------- /backend/models/magazines/index.ts: -------------------------------------------------------------------------------- 1 | import getMagazineById from "./getById"; 2 | import getMagazineCovers from "./getCovers"; 3 | 4 | export { getMagazineById, getMagazineCovers }; 5 | -------------------------------------------------------------------------------- /backend/models/news/getCovers.ts: -------------------------------------------------------------------------------- 1 | import prisma from "../../prisma"; 2 | 3 | const getNewsCovers = async (optObj: Object): Promise => { 4 | const result = await prisma.news.findMany(optObj); 5 | return result; 6 | }; 7 | 8 | export default getNewsCovers; 9 | -------------------------------------------------------------------------------- /backend/models/news/index.ts: -------------------------------------------------------------------------------- 1 | import getNewsById from "./getById"; 2 | import getNewsCovers from "./getCovers"; 3 | 4 | export { getNewsById, getNewsCovers }; 5 | -------------------------------------------------------------------------------- /backend/models/playlists/getCovers.ts: -------------------------------------------------------------------------------- 1 | import prisma from "../../prisma"; 2 | 3 | const getPlaylistCovers = async (optObj: any): Promise => { 4 | // eslint-disable-next-line no-param-reassign 5 | optObj.include = { 6 | Users: { 7 | select: { username: true }, 8 | }, 9 | }; 10 | const result = await prisma.playlists.findMany(optObj); 11 | return result; 12 | }; 13 | 14 | export default getPlaylistCovers; 15 | -------------------------------------------------------------------------------- /backend/models/playlists/index.ts: -------------------------------------------------------------------------------- 1 | import getPlaylistById from "./getById"; 2 | import getPlaylistCovers from "./getCovers"; 3 | import likePlaylist from "./likePlaylist"; 4 | import unlikePlaylist from "./unlikePlaylist"; 5 | 6 | export { getPlaylistById, getPlaylistCovers, likePlaylist, unlikePlaylist }; 7 | -------------------------------------------------------------------------------- /backend/models/playlists/likePlaylist.ts: -------------------------------------------------------------------------------- 1 | import prisma from "../../prisma"; 2 | 3 | const likePlaylist = async ( 4 | playlistId: number, 5 | user: any | undefined 6 | ): Promise => { 7 | const playlist: any = await prisma.users_Likes_Playlists.findFirst({ 8 | where: { playlistId }, 9 | }); 10 | if (playlist && !user) return null; 11 | 12 | const result = await prisma.users_Likes_Playlists.create({ 13 | data: { 14 | Playlists: { 15 | connect: { 16 | id: playlistId, 17 | }, 18 | }, 19 | Users: { 20 | connect: { 21 | id: user.id, 22 | }, 23 | }, 24 | }, 25 | }); 26 | 27 | return result; 28 | }; 29 | 30 | export default likePlaylist; 31 | -------------------------------------------------------------------------------- /backend/models/playlists/unlikePlaylist.ts: -------------------------------------------------------------------------------- 1 | import prisma from "../../prisma"; 2 | 3 | const unlikePlaylist = async ( 4 | playlistId: number, 5 | user: any | undefined 6 | ): Promise => { 7 | const playlist: any = await prisma.users_Likes_Playlists.findFirst({ 8 | where: { playlistId }, 9 | }); 10 | if (!playlist && !user) return null; 11 | 12 | const result = await prisma.users_Likes_Playlists.delete({ 13 | where: { 14 | id: playlist.id, 15 | }, 16 | }); 17 | 18 | return result; 19 | }; 20 | 21 | export default unlikePlaylist; 22 | -------------------------------------------------------------------------------- /backend/models/tracks/getTrackCard.ts: -------------------------------------------------------------------------------- 1 | import prisma from "../../prisma"; 2 | 3 | const getTrackCardData = async (id: number): Promise => { 4 | const track: any = await prisma.tracks.findUnique({ 5 | where: { id }, 6 | include: { Albums: true }, 7 | }); 8 | if (!track) return null; 9 | 10 | const artistIdArr = await prisma.artists_Tracks.findMany({ 11 | where: { trackId: id }, 12 | }); 13 | track.Artists = await Promise.all( 14 | artistIdArr.map((elem: any) => 15 | prisma.artists.findUnique({ where: { id: elem.artistId } }) 16 | ) 17 | ); 18 | 19 | return track; 20 | }; 21 | 22 | export default getTrackCardData; 23 | -------------------------------------------------------------------------------- /backend/models/tracks/index.ts: -------------------------------------------------------------------------------- 1 | import likeTrack from "./likeTrack"; 2 | import unlikeTrack from "./unlikeTrack"; 3 | import getTrackForSearch from "./getTrackForSearch"; 4 | import getTrackCard from "./getTrackCard"; 5 | 6 | export { likeTrack, unlikeTrack, getTrackForSearch, getTrackCard }; 7 | -------------------------------------------------------------------------------- /backend/models/tracks/likeTrack.ts: -------------------------------------------------------------------------------- 1 | import prisma from "../../prisma"; 2 | 3 | const likeTrack = async ( 4 | trackId: number, 5 | user: any | undefined 6 | ): Promise => { 7 | const track: any = await prisma.users_Like_Tracks.findFirst({ 8 | where: { trackId }, 9 | }); 10 | if (track && !user) return null; 11 | 12 | const result = await prisma.users_Like_Tracks.create({ 13 | data: { 14 | Tracks: { 15 | connect: { 16 | id: trackId, 17 | }, 18 | }, 19 | Users: { 20 | connect: { 21 | id: user.id, 22 | }, 23 | }, 24 | }, 25 | }); 26 | 27 | return result; 28 | }; 29 | 30 | export default likeTrack; 31 | -------------------------------------------------------------------------------- /backend/models/tracks/unlikeTrack.ts: -------------------------------------------------------------------------------- 1 | import prisma from "../../prisma"; 2 | 3 | const unlikeTrack = async ( 4 | trackId: number, 5 | user: any | undefined 6 | ): Promise => { 7 | const track: any = await prisma.users_Like_Tracks.findFirst({ 8 | where: { trackId }, 9 | }); 10 | if (!track && !user) return null; 11 | 12 | const result = await prisma.users_Like_Tracks.delete({ 13 | where: { 14 | id: track.id, 15 | }, 16 | }); 17 | 18 | return result; 19 | }; 20 | 21 | export default unlikeTrack; 22 | -------------------------------------------------------------------------------- /backend/models/users/index.ts: -------------------------------------------------------------------------------- 1 | import prisma from "../../prisma"; 2 | 3 | interface UserInfo { 4 | username?: string; 5 | password?: string; 6 | } 7 | 8 | interface returnInfo { 9 | id: number; 10 | username: string; 11 | } 12 | 13 | const postUserInfo = async ({ 14 | username, 15 | password, 16 | }: UserInfo): Promise => { 17 | if (!username || !password) return undefined; 18 | const user = await prisma.users.create({ data: { username, password } }); 19 | return { 20 | id: user.id, 21 | username, 22 | }; 23 | }; 24 | 25 | const getUserInfo = async ({ 26 | username, 27 | password, 28 | }: UserInfo): Promise => { 29 | const userInfo = await prisma.users.findFirst({ 30 | where: { username, password }, 31 | select: { 32 | id: true, 33 | username: true, 34 | }, 35 | }); 36 | return userInfo; 37 | }; 38 | 39 | const getUserInfoWithID = async (id: number): Promise => { 40 | const userInfo = await prisma.users.findFirst({ 41 | where: { id }, 42 | select: { 43 | id: true, 44 | username: true, 45 | profile: true, 46 | }, 47 | }); 48 | return userInfo; 49 | }; 50 | 51 | export { postUserInfo, getUserInfo, getUserInfoWithID }; 52 | -------------------------------------------------------------------------------- /backend/mongo.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { MongoClient } from "mongodb"; 3 | 4 | const db = {}; 5 | 6 | const connect = async (): Promise => { 7 | const mongoURL = process.env.MONGO_URL; 8 | const poolSize = process.env.MONGO_POOLSIZE 9 | ? parseInt(process.env.MONGO_POOLSIZE, 10) 10 | : 1; 11 | if (!mongoURL) { 12 | throw new Error("MONGO DB won't be connected!"); 13 | } 14 | const client = new MongoClient(mongoURL, { poolSize }); 15 | await client.connect(); 16 | const database = client.db("Vibe"); 17 | const iOS = database.collection("iOS"); 18 | const web = database.collection("web"); 19 | Object.assign(db, { iOS, web }); 20 | console.log("MongoDB is now connected"); 21 | }; 22 | 23 | export { connect, db }; 24 | -------------------------------------------------------------------------------- /backend/prisma/index.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | const prisma = new PrismaClient(); 4 | export default prisma; 5 | -------------------------------------------------------------------------------- /backend/prisma/migrations/migrate.lock: -------------------------------------------------------------------------------- 1 | # Prisma Migrate lockfile v1 2 | 3 | 20201206122741-add-profile-to-user -------------------------------------------------------------------------------- /backend/routes/albums/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import albumController from "../../controllers/albums"; 3 | 4 | const router = Router(); 5 | router.get("/", albumController.getAll); 6 | router.get("/:id", albumController.getAlbum); 7 | router.post("/like", albumController.like); 8 | router.post("/unlike", albumController.unlike); 9 | 10 | export default router; 11 | -------------------------------------------------------------------------------- /backend/routes/artists/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import artistController from "../../controllers/artists"; 3 | 4 | const router = Router(); 5 | router.get("/", artistController.getAll); 6 | router.get("/:id", artistController.getArtist); 7 | router.post("/like", artistController.like); 8 | router.post("/unlike", artistController.unlike); 9 | 10 | export default router; 11 | -------------------------------------------------------------------------------- /backend/routes/auth/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import authController from "../../controllers/auth"; 3 | 4 | const router = Router(); 5 | router.post("/signup", authController.signup); 6 | router.post("/login", authController.login); 7 | router.get("/naver", authController.naverCallback); 8 | router.get("/naverLogin", authController.naverRedirection); 9 | export default router; 10 | -------------------------------------------------------------------------------- /backend/routes/dj-stations/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import djStationController from "../../controllers/dj-stations"; 3 | 4 | const router = Router(); 5 | router.get("/", djStationController.getAll); 6 | 7 | export default router; 8 | -------------------------------------------------------------------------------- /backend/routes/genres/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import genreController from "../../controllers/genres"; 3 | 4 | const router = Router(); 5 | router.get("/", genreController.getAll); 6 | router.get("/:id", genreController.getGenre); 7 | 8 | export default router; 9 | -------------------------------------------------------------------------------- /backend/routes/index.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import tracksRouter from "./tracks"; 3 | import albumsRouter from "./albums"; 4 | import artistsRouter from "./artists"; 5 | import djStationsRouter from "./dj-stations"; 6 | import genresRouter from "./genres"; 7 | import magazinesRouter from "./magazines"; 8 | import newsRouter from "./news"; 9 | import playlistsRouter from "./playlists"; 10 | import libraryRouter from "./library"; 11 | import searchRouter from "./search"; 12 | import authRouter from "./auth"; 13 | import logRouter from "./log"; 14 | import usersRouter from "./users"; 15 | 16 | const router = express.Router(); 17 | 18 | router.use("/tracks", tracksRouter); 19 | router.use("/albums", albumsRouter); 20 | router.use("/artists", artistsRouter); 21 | router.use("/dj-stations", djStationsRouter); 22 | router.use("/genres", genresRouter); 23 | router.use("/magazines", magazinesRouter); 24 | router.use("/news", newsRouter); 25 | router.use("/playlists", playlistsRouter); 26 | router.use("/library", libraryRouter); 27 | router.use("/search", searchRouter); 28 | router.use("/auth", authRouter); 29 | router.use("/users", usersRouter); 30 | router.use("/log", logRouter); 31 | 32 | export default router; 33 | -------------------------------------------------------------------------------- /backend/routes/library/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import libraryController from "../../controllers/library"; 3 | 4 | const router = Router(); 5 | 6 | router.get("/albums", libraryController.getLibAlbums); 7 | router.get("/artists", libraryController.getLibArtists); 8 | router.get("/playlists", libraryController.getLibPlaylists); 9 | router.get("/tracks", libraryController.getLibTracks); 10 | 11 | router.post("/albums/:id", libraryController.postLibAlbums); 12 | router.post("/artists/:id", libraryController.postLibArtists); 13 | router.post("/playlists/:id", libraryController.postLibPlaylists); 14 | router.post("/tracks/:id", libraryController.postLibTracks); 15 | 16 | router.delete("/albums/:id", libraryController.deleteLibAlbums); 17 | router.delete("/artists/:id", libraryController.deleteLibArtists); 18 | router.delete("/playlists/:id", libraryController.deleteLibPlaylists); 19 | router.delete("/tracks/:id", libraryController.deleteLibTracks); 20 | export default router; 21 | -------------------------------------------------------------------------------- /backend/routes/log/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import logController from "../../controllers/log"; 3 | 4 | const router = Router(); 5 | router.get("/web", logController.getWebLog); 6 | router.get("/ios", logController.getiOSLog); 7 | router.post("/web", logController.postWebLog); 8 | router.post("/ios", logController.postiOSLog); 9 | 10 | export default router; 11 | -------------------------------------------------------------------------------- /backend/routes/magazines/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import magazineController from "../../controllers/magazines"; 3 | 4 | const router = Router(); 5 | router.get("/", magazineController.getAll); 6 | router.get("/:id", magazineController.getMagazine); 7 | 8 | export default router; 9 | -------------------------------------------------------------------------------- /backend/routes/news/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import newsController from "../../controllers/news"; 3 | 4 | const router = Router(); 5 | router.get("/", newsController.getAll); 6 | router.get("/:id", newsController.getNews); 7 | 8 | export default router; 9 | -------------------------------------------------------------------------------- /backend/routes/playlists/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import playlistController from "../../controllers/playlists"; 3 | 4 | const router = Router(); 5 | router.get("/", playlistController.getAll); 6 | router.get("/:id", playlistController.getPlaylist); 7 | router.post("/like", playlistController.like); 8 | router.post("/unlike", playlistController.unlike); 9 | 10 | export default router; 11 | -------------------------------------------------------------------------------- /backend/routes/search/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import searchController from "../../controllers/search"; 3 | 4 | const router = Router(); 5 | router.get("/", searchController.getAll); 6 | router.get("/tracks", searchController.getTracks); 7 | router.get("/artists", searchController.getArtists); 8 | router.get("/albums", searchController.getAlbums); 9 | 10 | export default router; 11 | -------------------------------------------------------------------------------- /backend/routes/tracks/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import trackController from "../../controllers/tracks"; 3 | 4 | const router = Router(); 5 | router.post("/like", trackController.like); 6 | router.post("/unlike", trackController.unlike); 7 | 8 | export default router; 9 | -------------------------------------------------------------------------------- /backend/routes/users/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import userController from "../../controllers/users"; 3 | 4 | const router = Router(); 5 | 6 | router.get("/profile", userController.getUserProfile); 7 | router.get("/likedItem", userController.getLikedItem); 8 | 9 | export default router; 10 | -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "ts-node": { 3 | "transpileOnly": true 4 | }, 5 | "compilerOptions": { 6 | "lib": ["ES5", "ES6"], 7 | "target": "es6", 8 | "module": "commonjs", 9 | "outDir": "./build", 10 | "esModuleInterop": true, 11 | "moduleResolution": "node", 12 | "emitDecoratorMetadata": true, 13 | "experimentalDecorators": true, 14 | "sourceMap": true, 15 | "strict": true, 16 | "skipLibCheck": true, 17 | "typeRoots": ["./node_modules/@types", "./customTypes"], 18 | }, 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /backend/utils/cookies.ts: -------------------------------------------------------------------------------- 1 | // import { NextApiRequest } from "next"; 2 | import { serialize } from "cookie"; 3 | // import NextApiResponseWithCookie from "../interfaces/NextApiResponseWithCookie"; 4 | 5 | interface CookieOption extends Object { 6 | maxAge: number; 7 | domain: string; 8 | } 9 | 10 | const cookie = ( 11 | res: any, 12 | name: string, 13 | value: string, 14 | options: CookieOption 15 | ): void => { 16 | res.setHeader("Set-Cookie", serialize(name, value, options)); 17 | }; 18 | 19 | const cookies = (handler: Function) => (req: any, res: any): Function => { 20 | res.cookie = (name: string, value: string, options: CookieOption) => 21 | cookie(res, name, value, options); 22 | 23 | return handler(req, res); 24 | }; 25 | 26 | export default cookies; 27 | -------------------------------------------------------------------------------- /backend/utils/decodeJWT.ts: -------------------------------------------------------------------------------- 1 | import { decode } from "jwt-simple"; 2 | 3 | interface UserInfo { 4 | id: number; 5 | username: string; 6 | } 7 | 8 | const decodeJWT = (jwt: string): UserInfo => { 9 | const secret = process.env.JWT_SECRET; 10 | return decode(jwt, secret || "dotenv is not working"); 11 | }; 12 | 13 | export default decodeJWT; 14 | -------------------------------------------------------------------------------- /backend/utils/encodeJWT.ts: -------------------------------------------------------------------------------- 1 | import { encode } from "jwt-simple"; 2 | 3 | const encodeJWT = (payload: any): string => { 4 | const secret = process.env.JWT_SECRET; 5 | if (typeof secret !== "string") 6 | return encode(payload, "dotenv is not working"); 7 | return encode(payload, secret); 8 | }; 9 | 10 | export default encodeJWT; 11 | -------------------------------------------------------------------------------- /backend/utils/makePrismaObtion.ts: -------------------------------------------------------------------------------- 1 | type optionObject = { 2 | take?: number; 3 | where?: Object; 4 | skip?: number; 5 | }; 6 | 7 | const makeOption = (_query: any, _target?: any, _type?: string): Object => { 8 | const { limit, filter } = _query; 9 | const optObj: optionObject = {}; 10 | 11 | if (limit) { 12 | optObj.take = +limit; 13 | } 14 | if (filter) { 15 | if (_type === "string") { 16 | optObj.where = { [_target]: filter }; 17 | } 18 | if (_type === "object") { 19 | optObj.where = _target; 20 | } 21 | if (_type === "number") { 22 | optObj.where = { [_target]: +filter }; 23 | } 24 | } 25 | 26 | return optObj; 27 | }; 28 | 29 | const makeSearchOption = (_query: any, _target: string): Object => { 30 | const { limit, filter, page } = _query; 31 | const optObj: optionObject = {}; 32 | 33 | if (limit) { 34 | optObj.take = +limit; 35 | } 36 | if (filter) { 37 | optObj.where = { 38 | [_target]: { contains: filter }, 39 | }; 40 | } 41 | if (!limit && page) { 42 | optObj.skip = (+page - 1) * 10; 43 | optObj.take = 10; 44 | } 45 | return optObj; 46 | }; 47 | 48 | export { makeOption, makeSearchOption }; 49 | -------------------------------------------------------------------------------- /frontend/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["next/babel"], 3 | "plugins": [ 4 | [ 5 | "styled-components", 6 | { 7 | "ssr": true, 8 | "displayName": true, 9 | "preprocess": false 10 | } 11 | ] 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /frontend/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": false, 3 | "semi": true, 4 | "useTabs": false, 5 | "tabWidth": 2, 6 | "trailingComma": "all", 7 | "printWidth": 100 8 | } 9 | -------------------------------------------------------------------------------- /frontend/.storybook/main.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | module.exports = { 3 | stories: ['../components/**/**/*.stories.tsx'], 4 | addons: ['@storybook/addon-essentials', '@storybook/addon-knobs/register', '@storybook/addon-controls'], 5 | webpackFinal: async config => { 6 | config.module.rules.push({ 7 | test: /\.(ts|tsx)$/, 8 | use: [ 9 | { 10 | loader: require.resolve('awesome-typescript-loader'), 11 | options:{ 12 | configFileName: path.resolve(__dirname, './tsconfig.json') 13 | } 14 | }, 15 | // Optional 16 | { 17 | loader: require.resolve('react-docgen-typescript-loader'), 18 | options:{ 19 | tsconfigPath: path.resolve(__dirname, './tsconfig.json'), 20 | 21 | } 22 | }, 23 | ], 24 | }); 25 | config.resolve.extensions.push('.ts', '.tsx'); 26 | return config; 27 | }, 28 | }; -------------------------------------------------------------------------------- /frontend/.storybook/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "alwaysStrict": true, 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "isolatedModules": true, 8 | "jsx": "preserve", 9 | "lib": [ 10 | "dom", 11 | "es2017" 12 | ], 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "noEmit": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "resolveJsonModule": true, 20 | "skipLibCheck": true, 21 | "strict": true, 22 | "target": "esnext", 23 | "experimentalDecorators": true, 24 | "emitDecoratorMetadata": true, 25 | "outDir": "ts-out/", 26 | "types" : [] 27 | }, 28 | "exclude": [ 29 | "node_modules" 30 | ], 31 | "include": [ 32 | "../types.d.ts", 33 | "../next-env.d.ts", 34 | "../**/*.stories.ts", 35 | "../**/*.stories.tsx" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12.18.3 2 | 3 | USER root 4 | 5 | MAINTAINER marullo Cho 6 | 7 | RUN mkdir -p /nextServer 8 | ADD . /nextServer 9 | WORKDIR /nextServer 10 | 11 | RUN npm install 12 | 13 | ARG SERVER_DOMAIN_PROD=/ 14 | ARG SERVER_DOMAIN_DEV=/ 15 | ARG NAVER_LOGIN_URL=/ 16 | ARG API_URL=/ 17 | ARG API_PORT=0 18 | 19 | ENV NEXT_PUBLIC_SERVER_DOMAIN_PRODUCTION=${SERVER_DOMAIN_PROD} 20 | ENV NEXT_PUBLIC_SERVER_DOMAIN_DEVELOP=${SERVER_DOMAIN_DEV} 21 | ENV NEXT_PUBLIC_NAVER_LOGIN_URL=${NAVER_LOGIN_URL} 22 | ENV API_URL=${API_URL} 23 | ENV API_PORT=${API_PORT} 24 | 25 | RUN npm run build 26 | 27 | EXPOSE 3000 28 | 29 | CMD "npm" "start" 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /frontend/components/Button/EllipsisButton/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/click-events-have-key-events */ 2 | /* eslint-disable jsx-a11y/no-static-element-interactions */ 3 | import React, { FC, MouseEvent } from "react"; 4 | import styled from "styled-components"; 5 | import icons from "../../../constant/icons"; 6 | 7 | const StyledEllipsisWrapper = styled.div` 8 | position: absolute; 9 | font-size: 1rem; 10 | bottom: 7.5%; 11 | right: 2rem; 12 | color: #aaa; 13 | z-index: 600; 14 | `; 15 | 16 | const StyledEllipsis = styled.div` 17 | display: flex; 18 | `; 19 | 20 | interface Props { 21 | onClick: (e: MouseEvent) => void; 22 | } 23 | 24 | const Ellipsis: FC = ({ onClick }: Props) => { 25 | return ( 26 | 27 | {icons.ellipsis} 28 | 29 | ); 30 | }; 31 | 32 | export default Ellipsis; 33 | -------------------------------------------------------------------------------- /frontend/components/Button/HeartButton/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const StyledEmptyHeart = styled.div` 4 | color: #555; 5 | font-size: 1.5rem; 6 | cursor: pointer; 7 | `; 8 | 9 | const StyledFilledHeart = styled.div` 10 | color: #fe1250; 11 | font-size: 1.5rem; 12 | cursor: pointer; 13 | `; 14 | 15 | export { StyledEmptyHeart, StyledFilledHeart }; 16 | -------------------------------------------------------------------------------- /frontend/components/Button/HoverEllipsisButton/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import HoverEllipsisButton, { StyledEllipsisButton } from "."; 3 | import { EllipsisSvg } from "../../../utils/svg"; 4 | 5 | export default { 6 | title: "EllipsisButton", 7 | component: HoverEllipsisButton, 8 | }; 9 | 10 | export const EllipsisButton: React.FC = () => { 11 | return ( 12 | 13 | 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /frontend/components/Button/HoverEllipsisButton/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { EllipsisSvg } from "../../../utils/svg"; 4 | import Ellipsis from "../EllipsisButton"; 5 | 6 | export const StyledEllipsisButton = styled.button` 7 | width: 2.5rem; 8 | height: 2.5rem; 9 | background-color: transparent; 10 | fill: white; 11 | position: absolute; 12 | border: none; 13 | outline: none; 14 | right: 10%; 15 | bottom: 10%; 16 | cursor: pointer; 17 | `; 18 | 19 | const HoverEllipsisButton: React.FC = () => { 20 | return ( 21 | 22 | 23 | 24 | ); 25 | }; 26 | 27 | export default HoverEllipsisButton; 28 | -------------------------------------------------------------------------------- /frontend/components/Button/HoverPlayButton/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import HoverPlayButton, { StyledPlayButton } from "."; 3 | import { PlaySvg } from "../../../utils/svg"; 4 | 5 | export default { 6 | title: "PlayButton", 7 | component: HoverPlayButton, 8 | }; 9 | 10 | export const PlayButton: React.FC = () => { 11 | return ( 12 | 13 | 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /frontend/components/Button/HoverPlayButton/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import icons from "../../../constant/icons"; 4 | 5 | const StyledPlayButton = styled.button` 6 | width: 2.5rem; 7 | height: 2.5rem; 8 | background-color: rgba(255, 255, 255, 0.7); 9 | border-radius: 50%; 10 | border: 0px; 11 | position: absolute; 12 | left: 7.5%; 13 | bottom: 7.5%; 14 | font-size: 1.25rem; 15 | color: #fe1250; 16 | outline: none; 17 | cursor: pointer; 18 | &:hover { 19 | background-color: rgba(255, 255, 255, 1); 20 | } 21 | `; 22 | 23 | const StyledText = styled.span` 24 | margin-left: 0.25rem; 25 | `; 26 | 27 | const HoverPlayButton: React.FC = () => { 28 | return ( 29 | 30 | {icons.play} 31 | 32 | ); 33 | }; 34 | 35 | export default HoverPlayButton; 36 | -------------------------------------------------------------------------------- /frontend/components/Card/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Story } from "@storybook/react/types-6-0"; 3 | import Card, { CardProps } from "."; 4 | 5 | export default { 6 | title: "Card", 7 | component: Card, 8 | }; 9 | 10 | const Template: Story = (args: any) => ; 11 | 12 | export const todayMagazineCard = Template.bind({}); 13 | todayMagazineCard.args = { 14 | rawData: { 15 | cover: 16 | "https://music-phinf.pstatic.net/20201119_255/1605768990292DkTAH_JPEG/%B4%EB%C7%A5-%C0%CC%B9%CC%C1%F61.jpg?type=w720", 17 | magazineName: `차트를 달리는 래퍼 18 | 잭 할로우, 물라토`, 19 | createdAt: new Date().toDateString(), 20 | playlistId: 1, 21 | }, 22 | varient: "todayBig", 23 | dataType: "magazine", 24 | }; 25 | -------------------------------------------------------------------------------- /frontend/components/Card/index.style.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | interface StyledCardProps { 4 | varient?: string; 5 | } 6 | 7 | const themes: any = { 8 | todayBig: ` 9 | width: 20rem; 10 | `, 11 | todaySmall: ` 12 | width: 12rem; 13 | `, 14 | todayNews: ` 15 | width: 20rem; 16 | `, 17 | trackCardCover: ` 18 | width: 2.5rem; 19 | `, 20 | magazineBig: ` 21 | width: 19rem; 22 | `, 23 | }; 24 | 25 | export const StyledCard = styled.li` 26 | position: relative; 27 | display: flex; 28 | flex-direction: column; 29 | margin-left: 1rem; 30 | margin-bottom: 2rem; 31 | ${(props) => themes[props.varient || ""]} 32 | & > a { 33 | text-decoration: none; 34 | } 35 | & > a:hover { 36 | text-decoration: underline; 37 | } 38 | `; 39 | 40 | export const TitleA = styled.a` 41 | font-size: 1rem; 42 | color: #222222; 43 | padding: 10px 0; 44 | `; 45 | export const SmallA = styled.a` 46 | font-size: 0.7rem; 47 | color: #777777; 48 | `; 49 | export const SmallSpan = styled.span` 50 | font-size: 0.7rem; 51 | color: #777777; 52 | `; 53 | 54 | export const StyledLinkDiv = styled.div` 55 | display: flex; 56 | cursor: pointer; 57 | `; 58 | -------------------------------------------------------------------------------- /frontend/components/ChartSlider/ChartTrackCard/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ChartTrackCard, { Props } from "."; 3 | 4 | export default { 5 | title: "ChartTrackCard", 6 | component: ChartTrackCard, 7 | }; 8 | 9 | export const ChartTrack = (args: Props) => ; 10 | ChartTrack.args = { 11 | trackName: "Dynamite", 12 | Albums: { 13 | id: 1, 14 | albumName: "MORE", 15 | description: "여왕 K/DA가 (여자)아이들의 미연과 소연", 16 | cover: 17 | "https://musicmeta-phinf.pstatic.net/album/005/055/5055232.jpg?type=r360Fll&v=20201029173916", 18 | artistId: 1, 19 | }, 20 | Artists: [ 21 | { 22 | id: 1, 23 | artistName: "우기", 24 | cover: 25 | "https://music-phinf.pstatic.net/20191121_58/1574322543698KbnOz_PNG/VIBE_WORKSTUDY_Lo-fi.png", 26 | }, 27 | { id: 2, artistName: "아이들", cover: null }, 28 | ], 29 | rank: 1, 30 | }; 31 | -------------------------------------------------------------------------------- /frontend/components/ChartSlider/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const StyledSlidebar = styled.div` 4 | display: flex; 5 | position: relative; 6 | &:nth-child(n) { 7 | padding-top: 1.5rem; 8 | padding-bottom: 2.5rem; 9 | border-bottom: 0.1rem solid rgba(0, 0, 0, 0.05); 10 | } 11 | `; 12 | 13 | const StyledSlidebarTitle = styled.div` 14 | font-size: 1.2rem; 15 | font-weight: bold; 16 | margin-top: 1rem; 17 | margin-bottom: 0.2rem; 18 | `; 19 | 20 | const SlideContainer = styled.div` 21 | overflow-x: hidden; 22 | margin: 1rem 0rem; 23 | width: 100%; 24 | height: 100%; 25 | `; 26 | 27 | const SlideContent = styled.ul` 28 | display: grid; 29 | grid-template-columns: repeat(20, minmax(50%, auto)); 30 | grid-template-rows: repeat(5, minmax(20%, auto)); 31 | grid-auto-flow: column; 32 | position: relative; 33 | width: 100%; 34 | height: 100%; 35 | padding-inline-start: 0; 36 | `; 37 | 38 | const StyledIcon = styled.span` 39 | margin-left: 0.3rem; 40 | `; 41 | 42 | export { StyledSlidebar, StyledSlidebarTitle, SlideContainer, SlideContent, StyledIcon }; 43 | -------------------------------------------------------------------------------- /frontend/components/DetailPage/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const StyledDetailPage = styled.div` 4 | display: flex; 5 | margin: 3rem; 6 | flex-direction: column; 7 | `; 8 | 9 | const StyledDescriptionHeader = styled.div` 10 | display: flex; 11 | justify-content: flex-start; 12 | align-items: center; 13 | width: 100%; 14 | padding-bottom: 3rem; 15 | box-sizing: border-box; 16 | border-bottom: 0.1rem solid #ddd; 17 | `; 18 | 19 | const StyledTrackCard = styled.div` 20 | display: flex; 21 | margin-top: 1rem; 22 | `; 23 | 24 | export { StyledDetailPage, StyledDescriptionHeader, StyledTrackCard }; 25 | -------------------------------------------------------------------------------- /frontend/components/GenreContainer/GenreCard/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Story } from "@storybook/react/types-6-0"; 3 | import GenreCard, { Props } from "."; 4 | 5 | export default { 6 | title: "GenreCard", 7 | component: GenreCard, 8 | }; 9 | 10 | export const Template: Story = (args: any) => ; 11 | Template.args = { 12 | data: { 13 | id: 1, 14 | genreName: "국내 댄스", 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /frontend/components/GenreContainer/GenreCard/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | export interface Props { 5 | data: { 6 | id?: number; 7 | genreName?: string; 8 | genreColor: string; 9 | }; 10 | } 11 | 12 | interface GenreColorProps { 13 | genreColor?: string; 14 | } 15 | 16 | const StyledGenreCard = styled.li` 17 | display: flex; 18 | align-items: center; 19 | background-color: #ddd; 20 | width: 11rem; 21 | height: 4rem; 22 | border-radius: 0.3rem; 23 | `; 24 | 25 | const ColorDiv = styled.div` 26 | width: 0.25rem; 27 | height: 80%; 28 | border-radius: 0.1rem; 29 | margin: 5% 7px; 30 | background-color: ${(props) => props.genreColor}; 31 | `; 32 | const StyledGenreCardName = styled.div` 33 | margin-left: 10px; 34 | text-align: left; 35 | padding: 1.2rrem 0; 36 | height: 1.5rem; 37 | `; 38 | 39 | const GenreCard: React.FC = ({ data }: Props) => { 40 | const { genreName, genreColor } = data; 41 | return ( 42 | 43 | 44 | {genreName} 45 | 46 | ); 47 | }; 48 | 49 | export default GenreCard; 50 | -------------------------------------------------------------------------------- /frontend/components/HotMagCard/HotMagCard.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Story } from "@storybook/react/types-6-0"; 2 | import HotMagCard from "."; 3 | 4 | export default { 5 | title: "HotMagCard", 6 | component: HotMagCard, 7 | }; 8 | 9 | const Template: Story = () => ; 10 | 11 | const HotMagCardTemplate = Template.bind({}); 12 | 13 | export { HotMagCardTemplate }; 14 | -------------------------------------------------------------------------------- /frontend/components/HoverImg/TrackCardHoverCover/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import icons from "../../../constant/icons"; 4 | import { PlaySvg } from "../../../utils/svg"; 5 | 6 | interface StyleProps { 7 | hover?: boolean; 8 | } 9 | 10 | interface Props { 11 | hover?: boolean; 12 | } 13 | 14 | const StyledTrackCardHoverCover = styled.div` 15 | display: flex; 16 | width: 2.5rem; 17 | height: 2.5rem; 18 | cursor: pointer; 19 | position: absolute; 20 | justify-content: center; 21 | align-items: center; 22 | top: 0rem; 23 | left: 0rem; 24 | color: #fff; 25 | z-index: 1; 26 | background-color: rgba(0, 0, 0, 0.5); 27 | display: ${(props) => (props.hover ? "block" : "none")}; 28 | `; 29 | 30 | const StyledIcon = styled.div` 31 | display: flex; 32 | height: 100%; 33 | justify-content: center; 34 | align-items: center; 35 | `; 36 | 37 | const TrackCardHoverCover: React.FC = ({ hover }: Props) => { 38 | return ( 39 | 40 | {icons.play} 41 | 42 | ); 43 | }; 44 | 45 | export default TrackCardHoverCover; 46 | -------------------------------------------------------------------------------- /frontend/components/HoverImg/img.style.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export interface styledHoverImgProps { 4 | varient?: string; 5 | } 6 | 7 | const HoverImgthemes: any = { 8 | todayBig: ` 9 | width: 20rem; 10 | height: 20rem; 11 | `, 12 | todaySmall: ` 13 | width: 12rem; 14 | height: 12rem; 15 | `, 16 | todayNews: ` 17 | width: 20rem; 18 | height: 12rem; 19 | `, 20 | trackCardCover: ` 21 | width: 2.5rem; 22 | height: 2.5rem; 23 | `, 24 | magazineBig: ` 25 | width: 19rem; 26 | height: 19rem; 27 | `, 28 | }; 29 | 30 | export const StyledHoverImg = styled.div` 31 | object-fit: cover; 32 | display: flex; 33 | position: relative; 34 | ${(props) => HoverImgthemes[props.varient || ""]} 35 | `; 36 | -------------------------------------------------------------------------------- /frontend/components/HoverImg/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import styled from "styled-components"; 3 | import Img from "../Img"; 4 | import GeneralHoverCover from "./GeneralHoverCover"; 5 | import { StyledHoverImg } from "./img.style"; 6 | import TrackCardHoverCover from "./TrackCardHoverCover"; 7 | 8 | export interface HoverImgProps { 9 | varient?: string; 10 | heartType: "Tracks" | "Albums" | "Playlists" | "Artists"; 11 | heartId: number; 12 | src?: string; 13 | } 14 | 15 | const HoverImg: React.FC = ({ varient, src, heartType, heartId }: HoverImgProps) => { 16 | const [hover, setHover] = useState(false); 17 | 18 | const onHover = () => { 19 | setHover(true); 20 | }; 21 | 22 | const onHoverOut = () => { 23 | setHover(false); 24 | }; 25 | 26 | return ( 27 | 28 | 29 | {varient === "trackCardCover" ? ( 30 | 31 | ) : ( 32 | 33 | )} 34 | 35 | ); 36 | }; 37 | 38 | export default HoverImg; 39 | -------------------------------------------------------------------------------- /frontend/components/Img/Img.style.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | interface styledImgProps { 4 | varient?: string; 5 | hover?: boolean; 6 | } 7 | 8 | const themes: any = { 9 | todayBig: ` 10 | width: 20rem; 11 | height: 20rem; 12 | `, 13 | todaySmall: ` 14 | width: 12rem; 15 | height: 12rem; 16 | `, 17 | todayNews: ` 18 | min-width: 20rem; 19 | height: 12rem; 20 | `, 21 | descriptionCover: ` 22 | width: 13rem; 23 | height: 13rem; 24 | `, 25 | profile: ` 26 | width: 2rem; 27 | height: 2rem; 28 | border-radius: 50%; 29 | `, 30 | trackCardCover: ` 31 | width: 2.5rem; 32 | height: 2.5rem; 33 | `, 34 | likedArtist: ` 35 | width: 12rem; 36 | height: 12rem; 37 | border-radius: 50%; 38 | `, 39 | nowPlayingCover: ` 40 | width: 3rem; 41 | height: 3rem; 42 | `, 43 | magazineBig: ` 44 | width: 19rem; 45 | height: 19rem; 46 | `, 47 | }; 48 | 49 | const StyledImg = styled.img` 50 | object-fit: cover; 51 | &:hover { 52 | filter: ${(props) => (props.hover ? "brightness(0.9)" : "none")}; 53 | } 54 | ${(props) => { 55 | return themes[props.varient || ""]; 56 | }} 57 | `; 58 | 59 | export default StyledImg; 60 | -------------------------------------------------------------------------------- /frontend/components/Img/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Story } from "@storybook/react/types-6-0"; 3 | import Img, { ImgProps } from "."; 4 | 5 | export default { 6 | title: "Img", 7 | component: Img, 8 | }; 9 | 10 | const Template: Story = (args: any) => ; 11 | 12 | export const TemplateImg = Template.bind({}); 13 | 14 | export const TodayBigImg = Template.bind({}); 15 | TodayBigImg.args = { varient: "todayBig" }; 16 | 17 | export const TodaySmallImg = Template.bind({}); 18 | TodaySmallImg.args = { varient: "todaySmall" }; 19 | 20 | export const TodayNewsImg = Template.bind({}); 21 | TodayNewsImg.args = { varient: "todayNews" }; 22 | 23 | export const descriptionCoverImg = Template.bind({}); 24 | descriptionCoverImg.args = { varient: "descriptionCover" }; 25 | 26 | export const ProfileImg = Template.bind({}); 27 | ProfileImg.args = { varient: "profile" }; 28 | 29 | export const TrackCardImg = Template.bind({}); 30 | TrackCardImg.args = { varient: "trackCardCover" }; 31 | 32 | export const LikedArtistImg = Template.bind({}); 33 | LikedArtistImg.args = { varient: "likedArtist" }; 34 | 35 | export const NowPlayingImg = Template.bind({}); 36 | NowPlayingImg.args = { varient: "nowPlayingCover" }; 37 | -------------------------------------------------------------------------------- /frontend/components/Img/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import StyledImg from "./Img.style"; 3 | 4 | export interface ImgProps { 5 | varient?: string; 6 | src?: string; 7 | hover?: boolean; 8 | } 9 | 10 | const Img: React.FC = ({ varient, src, hover }: ImgProps) => { 11 | return ; 12 | }; 13 | 14 | export default Img; 15 | -------------------------------------------------------------------------------- /frontend/components/MagLabel/MagLabel.interface.ts: -------------------------------------------------------------------------------- 1 | interface MagLabelStyles { 2 | backgroundColor?: string; 3 | backgroundImage?: string; 4 | width: number; 5 | } 6 | 7 | interface MagLabelProps extends MagLabelStyles { 8 | children: string; 9 | className?: string; 10 | } 11 | 12 | export type { MagLabelStyles, MagLabelProps }; 13 | -------------------------------------------------------------------------------- /frontend/components/MagLabel/MagLabel.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Story } from "@storybook/react/types-6-0"; 3 | import { MagLabelProps } from "./MagLabel.interface"; 4 | import { 5 | AllMagLabelStyles, 6 | SpecialMagLabelStyles, 7 | PickMagLabelStyles, 8 | GenreMagLabelStyles, 9 | } from "./MagLabel.style"; 10 | import MagLabel from "."; 11 | 12 | export default { 13 | title: "MagLabel", 14 | component: MagLabel, 15 | }; 16 | 17 | const Template: Story = (args: any) => ; 18 | 19 | const AllMagLabel = Template.bind({}); 20 | AllMagLabel.args = { ...AllMagLabelStyles, children: "전체" }; 21 | 22 | const SpecialMagLabel = Template.bind({}); 23 | SpecialMagLabel.args = { ...SpecialMagLabelStyles, children: "Special" }; 24 | 25 | const PickMagLabel = Template.bind({}); 26 | PickMagLabel.args = { ...PickMagLabelStyles, children: "PICK" }; 27 | 28 | const GenreMagLabel = Template.bind({}); 29 | GenreMagLabel.args = { ...GenreMagLabelStyles, children: "GENRE" }; 30 | 31 | export { AllMagLabel, SpecialMagLabel, PickMagLabel, GenreMagLabel }; 32 | -------------------------------------------------------------------------------- /frontend/components/MagLabel/MagLabel.style.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { MagLabelStyles } from "./MagLabel.interface"; 3 | 4 | const StyledMagLabel = styled.div` 5 | height: 0.75rem; 6 | width: ${({ width }) => width}rem; 7 | padding: 0.5rem; 8 | background-color: ${({ backgroundColor, backgroundImage }) => 9 | backgroundImage ? "" : backgroundColor}; 10 | background-image: ${({ backgroundColor, backgroundImage }) => 11 | backgroundColor ? "" : backgroundImage}; 12 | color: #fff; 13 | border-radius: 1.6rem; 14 | font-size: 0.75rem; 15 | font-weight: bold; 16 | text-align: center; 17 | `; 18 | 19 | export default StyledMagLabel; 20 | 21 | const AllMagLabelStyles: MagLabelStyles = { width: 2, backgroundColor: "#FF0350" }; 22 | 23 | const SpecialMagLabelStyles: MagLabelStyles = { 24 | width: 5, 25 | backgroundImage: "linear-gradient(#e66465, #9198e5)", 26 | }; 27 | 28 | const PickMagLabelStyles: MagLabelStyles = { 29 | width: 3.3, 30 | backgroundColor: "#FF0350", 31 | }; 32 | 33 | const GenreMagLabelStyles: MagLabelStyles = { 34 | width: 4.8, 35 | backgroundColor: "#8B02ED", 36 | }; 37 | 38 | export { AllMagLabelStyles, SpecialMagLabelStyles, PickMagLabelStyles, GenreMagLabelStyles }; 39 | -------------------------------------------------------------------------------- /frontend/components/NavBar/LinkCardBlock.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Story } from "@storybook/react/types-6-0"; 2 | import NavBar, { Theme } from "."; 3 | 4 | export default { 5 | title: "NavBar", 6 | component: NavBar, 7 | }; 8 | 9 | const Template: Story<{}> = () => ; 10 | 11 | const NavBarTemplate = Template.bind({}); 12 | NavBarTemplate.args = { theme: Theme.Main }; 13 | 14 | export { NavBarTemplate }; 15 | -------------------------------------------------------------------------------- /frontend/components/NavBar/LinkCardBlock/LinkCard/LinkCard.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Story } from "@storybook/react/types-6-0"; 2 | import LinkCard, { Props, Theme } from "."; 3 | 4 | export default { 5 | title: "LinkCard", 6 | component: LinkCard, 7 | }; 8 | 9 | const Template: Story = (args: Props) => ( 10 | 11 | {args.children} 12 | 13 | ); 14 | 15 | const MainLink = Template.bind({}); 16 | MainLink.args = { theme: Theme.Main, children: "DJ Stations", icon: "dj" }; 17 | 18 | const LibraryLink = Template.bind({}); 19 | LibraryLink.args = { theme: Theme.Library, children: "플레이리스트" }; 20 | 21 | export { MainLink, LibraryLink }; 22 | -------------------------------------------------------------------------------- /frontend/components/NavBar/LinkCardBlock/LinkCard/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, MouseEvent } from "react"; 2 | import { useRouter } from "next/router"; 3 | import StyledLinkCard from "./styled"; 4 | 5 | export enum Theme { 6 | Main = "main", 7 | Library = "library", 8 | } 9 | 10 | interface Props { 11 | theme: Theme; 12 | children: string; 13 | href: string; 14 | icon?: string; 15 | } 16 | 17 | const LinkCard = memo(({ theme, children, icon, href }: Props) => { 18 | const router = useRouter(); 19 | const handleRouter = (e: MouseEvent) => { 20 | e.preventDefault(); 21 | router.push(href); 22 | }; 23 | return ( 24 | 25 | 26 | {children} 27 | 28 | 29 | ); 30 | }); 31 | 32 | export default LinkCard; 33 | export type { Props }; 34 | -------------------------------------------------------------------------------- /frontend/components/NavBar/LinkCardBlock/LinkCard/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { Theme } from "."; 3 | 4 | const icons: any = { 5 | home: "🏠", 6 | chart: "🏆", 7 | dj: "📀", 8 | mag: "📖", 9 | }; 10 | 11 | interface StyledProps { 12 | theme: string; 13 | icon: string; 14 | } 15 | 16 | const StyledLinkCard = styled.div` 17 | color: #ccc; 18 | &:hover { 19 | color: #fff; 20 | } 21 | ${({ theme, icon }: StyledProps) => { 22 | switch (theme) { 23 | case Theme.Library: 24 | return ` 25 | height: 1rem; 26 | font-size: 1rem; 27 | && { 28 | margin-right: 0.75rem; 29 | margin-top: 1rem; 30 | }; 31 | `; 32 | default: 33 | return ` 34 | height: 1.25rem; 35 | font-size: 1.25rem; 36 | && { 37 | margin-right: 1rem; 38 | margin-top: 1rem; 39 | } 40 | 41 | &::before { 42 | content: "${icons[icon]}"; 43 | margin-right: 0.5rem; 44 | } 45 | `; 46 | } 47 | }} 48 | `; 49 | 50 | export default StyledLinkCard; 51 | -------------------------------------------------------------------------------- /frontend/components/NavBar/LinkCardBlock/LinkCardBlock.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Story } from "@storybook/react/types-6-0"; 2 | import LinkCardBlock, { Props, Theme } from "."; 3 | 4 | export default { 5 | title: "LinkCardBlock", 6 | component: LinkCardBlock, 7 | }; 8 | 9 | const Template: Story = (args: Props) => ; 10 | 11 | const MainLinkBlock = Template.bind({}); 12 | MainLinkBlock.args = { theme: Theme.Main }; 13 | 14 | const LibraryLinkBlock = Template.bind({}); 15 | LibraryLinkBlock.args = { theme: Theme.Library }; 16 | 17 | export { MainLinkBlock, LibraryLinkBlock }; 18 | -------------------------------------------------------------------------------- /frontend/components/NavBar/LinkCardBlock/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const StyledLinkCardBlock = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | `; 7 | 8 | export default StyledLinkCardBlock; 9 | -------------------------------------------------------------------------------- /frontend/components/NavBar/NavBarUser/NavBarUser.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Story } from "@storybook/react/types-6-0"; 2 | import NavBarUser from "."; 3 | 4 | export default { 5 | title: "NavBarUser", 6 | component: NavBarUser, 7 | }; 8 | 9 | const Template: Story<{}> = () => ; 10 | 11 | const NavBarUserTemplate = Template.bind({}); 12 | 13 | export { NavBarUserTemplate }; 14 | -------------------------------------------------------------------------------- /frontend/components/NavBar/NavTopLogoSearch/NavTopLogoSearch.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Story } from "@storybook/react/types-6-0"; 2 | import NavTopLogoSearch from "."; 3 | 4 | export default { 5 | title: "NavTopLogoSearch", 6 | component: NavTopLogoSearch, 7 | }; 8 | 9 | const Template: Story<{}> = () => ; 10 | 11 | const NavTopLogoSearchTemplate = Template.bind({}); 12 | 13 | export { NavTopLogoSearchTemplate }; 14 | -------------------------------------------------------------------------------- /frontend/components/NavBar/NavTopLogoSearch/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from "react"; 2 | import { useRouter } from "next/router"; 3 | import icons from "../../../constant/icons"; 4 | import { 5 | StyledNavTopLogoSearch, 6 | StyledNavLogo, 7 | StyledNavVIBE, 8 | StyledWith, 9 | StyledNavDIVE, 10 | StyledNavSearch, 11 | } from "./styled"; 12 | 13 | const NavTopLogoSearch = memo( 14 | ({ handleSearch }: { handleSearch: () => void }): React.ReactElement => { 15 | const router = useRouter(); 16 | 17 | return ( 18 | 19 | router.push("/today")}> 20 | VIBE 21 | with 22 | DIVE 23 | 24 | {icons.search} 25 | 26 | ); 27 | }, 28 | ); 29 | 30 | export default NavTopLogoSearch; 31 | -------------------------------------------------------------------------------- /frontend/components/NavBar/NavTopLogoSearch/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const StyledNavTopLogoSearch = styled.div` 4 | display: flex; 5 | height: 3rem; 6 | `; 7 | 8 | const StyledNavLogo = styled.div` 9 | display: flex; 10 | align-items: center; 11 | width: 88%; 12 | color: #fff; 13 | font-weight: bold; 14 | cursor: pointer; 15 | `; 16 | 17 | const StyledNavVIBE = styled.span` 18 | font-size: 1rem; 19 | padding-left: 0.5rem; 20 | padding-top: 0.5rem; 21 | `; 22 | 23 | const StyledWith = styled.span` 24 | font-size: 1rem; 25 | margin-left: 0.25rem; 26 | padding-right: 0.25rem; 27 | padding-top: 0.5rem; 28 | font-style: italic; 29 | `; 30 | 31 | const StyledNavDIVE = styled.span` 32 | font-size: 1.75rem; 33 | `; 34 | 35 | const StyledNavSearch = styled.div` 36 | display: flex; 37 | align-items: center; 38 | width: 12%; 39 | color: #fff; 40 | cursor: pointer; 41 | `; 42 | 43 | export { 44 | StyledNavTopLogoSearch, 45 | StyledNavLogo, 46 | StyledNavVIBE, 47 | StyledWith, 48 | StyledNavDIVE, 49 | StyledNavSearch, 50 | }; 51 | -------------------------------------------------------------------------------- /frontend/components/NavBar/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo, useState, useEffect } from "react"; 2 | import LinkCardBlock from "./LinkCardBlock"; 3 | import NavTopLogoSearch from "./NavTopLogoSearch"; 4 | import NavBarUser from "./NavBarUser"; 5 | import { StyledNavBar, StyledLibrary, StyledLibraryText } from "./styled"; 6 | 7 | interface Props { 8 | handleSearch: () => void; 9 | } 10 | 11 | export enum Theme { 12 | Main = "main", 13 | Library = "library", 14 | } 15 | 16 | const NavBar: FC = memo(({ handleSearch }: Props) => { 17 | const [isLogged, setIsLogged] = useState(false); 18 | 19 | useEffect(() => { 20 | localStorage.getItem("token"); 21 | }, []); 22 | 23 | return ( 24 | 25 | 26 | 27 | 28 | 29 | {isLogged && ( 30 | 31 | 보관함 32 | 33 | 34 | )} 35 | 36 | ); 37 | }); 38 | 39 | export default NavBar; 40 | -------------------------------------------------------------------------------- /frontend/components/NavBar/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const StyledNavBar = styled.div` 4 | display: flex; 5 | position: fixed; 6 | flex-direction: column; 7 | background-color: #000; 8 | min-height: 100%; 9 | box-sizing: border-box; 10 | padding: 1.5rem; 11 | width: 15%; 12 | z-index: 50; 13 | `; 14 | 15 | const StyledLibrary = styled.div` 16 | display: flex; 17 | flex-direction: column; 18 | margin-top: 2rem; 19 | `; 20 | 21 | const StyledLibraryText = styled.div` 22 | display: flex; 23 | font-size: 0.9rem; 24 | color: #444; 25 | `; 26 | 27 | export { StyledNavBar, StyledLibrary, StyledLibraryText }; 28 | -------------------------------------------------------------------------------- /frontend/components/SearchBar/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const StyledSearchBar = styled.div` 4 | display: flex; 5 | width: 95%; 6 | height: 75%; 7 | border: 0.25rem solid #b6daff; 8 | border-radius: 0.75rem; 9 | justify-content: center; 10 | align-items: center; 11 | `; 12 | 13 | const StyledSearchIcon = styled.div` 14 | display: flex; 15 | width: 3%; 16 | justify-content: center; 17 | align-items: center; 18 | `; 19 | 20 | const StyledSearchInput = styled.input` 21 | display: flex; 22 | width: 94%; 23 | height: 75%; 24 | font-size: 1.5rem; 25 | caret-color: #ff015f; 26 | border: none; 27 | outline: none; 28 | `; 29 | 30 | const StyledClosingIcon = styled.div` 31 | display: flex; 32 | width: 3%; 33 | justify-content: center; 34 | align-items: center; 35 | `; 36 | 37 | const StyledText = styled.div` 38 | font-size: 1.5rem; 39 | color: #777; 40 | `; 41 | 42 | export { StyledSearchBar, StyledSearchIcon, StyledSearchInput, StyledClosingIcon, StyledText }; 43 | -------------------------------------------------------------------------------- /frontend/components/SearchSamples/SearchAlbumList/SearchAlbumCard/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import Link from "next/link"; 3 | import { StyledAlbum, StyledCover, StyledAlbumName, StyledAlbumArtist } from "./styled"; 4 | import Img from "../../../Img"; 5 | 6 | interface Props { 7 | albumName: string; 8 | artistName: string; 9 | cover: string; 10 | id: number; 11 | artistId: number; 12 | } 13 | 14 | const SearchAlbumCard: FC = ({ albumName, artistName, cover, id, artistId }: Props) => { 15 | return ( 16 | 17 | 18 | 19 | 20 | 21 | {albumName} 22 | 23 | 24 | {artistName} 25 | 26 | 27 | ); 28 | }; 29 | 30 | export default SearchAlbumCard; 31 | -------------------------------------------------------------------------------- /frontend/components/SearchSamples/SearchAlbumList/SearchAlbumCard/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const StyledAlbum = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | width: 12rem; 9 | height: 15rem; 10 | & + & { 11 | margin: 3rem; // TODO: 더 많은 데이터가 있을 때 간격 다시 확인해보기 12 | } 13 | `; 14 | 15 | const StyledCover = styled.div` 16 | display: flex; 17 | flex-direction: column; 18 | justify-content: center; 19 | width: 12rem; 20 | `; 21 | 22 | const StyledAlbumName = styled.div` 23 | display: flex; 24 | justify-content: flex-start; 25 | align-items: center; 26 | font-size: 1rem; 27 | width: 12rem; 28 | box-sizing: border-box; 29 | font-size: 1.1rem; 30 | padding: 0.5rem 0rem; 31 | cursor: pointer; 32 | &:hover { 33 | text-decoration: underline; 34 | } 35 | `; 36 | 37 | const StyledAlbumArtist = styled.div` 38 | display: flex; 39 | justify-content: flex-start; 40 | align-items: center; 41 | font-size: 1rem; 42 | color: #aaa; 43 | width: 12rem; 44 | box-sizing: border-box; 45 | font-size: 1.1rem; 46 | cursor: pointer; 47 | &:hover { 48 | text-decoration: underline; 49 | } 50 | `; 51 | 52 | export { StyledAlbum, StyledCover, StyledAlbumName, StyledAlbumArtist }; 53 | -------------------------------------------------------------------------------- /frontend/components/SearchSamples/SearchAlbumList/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import StyledAlbumList from "./styled"; 3 | import SearchAlbumCard from "./SearchAlbumCard"; 4 | 5 | interface Album { 6 | id: number; 7 | albumName: string; 8 | description: string; 9 | cover: string; 10 | artistId: number; 11 | Artists: { 12 | artistName: string; 13 | }; 14 | } 15 | 16 | interface Props { 17 | data: Album[]; 18 | } 19 | 20 | const SearchAlbumList: FC = ({ data }: Props) => { 21 | return ( 22 | 23 | {data.map((album) => { 24 | const { 25 | albumName, 26 | cover, 27 | Artists: { artistName }, 28 | id, 29 | artistId, 30 | } = album; 31 | return ( 32 | 40 | ); 41 | })} 42 | 43 | ); 44 | }; 45 | 46 | export default SearchAlbumList; 47 | -------------------------------------------------------------------------------- /frontend/components/SearchSamples/SearchAlbumList/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const StyledAlbums = styled.div` 4 | width: 100%; 5 | display: flex; 6 | flex-wrap: wrap; 7 | `; 8 | 9 | export default StyledAlbums; 10 | -------------------------------------------------------------------------------- /frontend/components/SearchSamples/SearchArtistList/SearchArtistCard/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import Link from "next/link"; 3 | import { StyledArtist, StyledCover, StyledArtistName } from "./styled"; 4 | import Img from "../../../Img"; 5 | 6 | interface Props { 7 | artistName: string; 8 | cover: string; 9 | id: number; // artistId 10 | } 11 | 12 | const SearchArtistCard: FC = ({ artistName, cover, id }: Props) => { 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 | {artistName} 20 | 21 | 22 | ); 23 | }; 24 | 25 | export default SearchArtistCard; 26 | -------------------------------------------------------------------------------- /frontend/components/SearchSamples/SearchArtistList/SearchArtistCard/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const StyledArtist = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | width: 12rem; 9 | height: 15rem; 10 | & + & { 11 | margin: 3rem; // TODO: 더 많은 데이터가 있을 때 간격 다시 확인해보기 12 | } 13 | `; 14 | 15 | const StyledCover = styled.div` 16 | display: flex; 17 | flex-direction: column; 18 | justify-content: center; 19 | width: 12rem; 20 | `; 21 | 22 | const StyledArtistName = styled.div` 23 | display: flex; 24 | justify-content: center; 25 | align-items: center; 26 | font-size: 1rem; 27 | width: 12rem; 28 | box-sizing: border-box; 29 | font-size: 1.1rem; 30 | padding: 0.5rem 0rem; 31 | cursor: pointer; 32 | &:hover { 33 | text-decoration: underline; 34 | } 35 | `; 36 | 37 | export { StyledArtist, StyledCover, StyledArtistName }; 38 | -------------------------------------------------------------------------------- /frontend/components/SearchSamples/SearchArtistList/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import SearchArtistCard from "./SearchArtistCard"; 3 | import StyledArtistList from "./styled"; 4 | 5 | interface Artist { 6 | artistName: string; 7 | cover: string; 8 | id: number; // artistId 9 | description: string; 10 | } 11 | 12 | interface Props { 13 | data: Artist[]; 14 | } 15 | 16 | const SearchArtistList: FC = ({ data }: Props) => { 17 | return ( 18 | 19 | {data.map((artist) => { 20 | const { artistName, cover, id } = artist; 21 | return ; 22 | })} 23 | 24 | ); 25 | }; 26 | 27 | export default SearchArtistList; 28 | -------------------------------------------------------------------------------- /frontend/components/SearchSamples/SearchArtistList/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const StyledArtistList = styled.div` 4 | width: 100%; 5 | display: flex; 6 | flex-wrap: wrap; 7 | `; 8 | 9 | export default StyledArtistList; 10 | -------------------------------------------------------------------------------- /frontend/components/Slidebar/func/onPreviousClicked.ts: -------------------------------------------------------------------------------- 1 | interface Props { 2 | setCurrentTranslateX: Function; 3 | setPreviousHide: Function; 4 | setNextHide: Function; 5 | currentTranslateX: number; 6 | slidePixels: number; 7 | } 8 | 9 | const onPreviousClicked = ({ 10 | setCurrentTranslateX, 11 | setPreviousHide, 12 | setNextHide, 13 | currentTranslateX, 14 | slidePixels, 15 | }: Props): void => { 16 | const newTranslateX = currentTranslateX + slidePixels; 17 | if (newTranslateX > 0) { 18 | setCurrentTranslateX(0); 19 | setPreviousHide(true); 20 | setNextHide(false); 21 | return; 22 | } 23 | setCurrentTranslateX(newTranslateX); 24 | setNextHide(false); 25 | }; 26 | 27 | export default onPreviousClicked; 28 | -------------------------------------------------------------------------------- /frontend/components/Slidebar/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { SlidebarProps, TranslateProps } from "./index"; 3 | 4 | const StyledSlidebar = styled.div` 5 | display: flex; 6 | flex-direction: column; 7 | width: 100%; 8 | height: auto; 9 | `; 10 | 11 | const SlideContainer = styled.div` 12 | overflow: auto; 13 | width: 100%; 14 | position: relative; 15 | &::-webkit-scrollbar { 16 | display: none; 17 | } 18 | `; 19 | 20 | const StyledTitle = styled.div` 21 | font-size: 1.3rem; 22 | font-weight: bold; 23 | margin: 1rem 0rem; 24 | `; 25 | 26 | const SlideContent = styled.ul` 27 | display: flex; 28 | & > li:first-child { 29 | margin: 0; 30 | } 31 | padding-inline-start: 0; 32 | transition: all 0.6s ease-in-out; 33 | transform: ${({ currentTranslateX }) => `translateX(${currentTranslateX}px)`}; 34 | `; 35 | 36 | const StyledLink = styled.div` 37 | display: flex; 38 | cursor: pointer; 39 | `; 40 | 41 | const StyledIcon = styled.span` 42 | margin-left: 0.3rem; 43 | `; 44 | 45 | export { StyledSlidebar, SlideContainer, StyledTitle, SlideContent, StyledLink, StyledIcon }; 46 | -------------------------------------------------------------------------------- /frontend/components/Tracklist/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import { Track } from "../../interfaces"; 3 | import StyledTrackCards from "./styled"; 4 | import TrackCard from "./TrackCard"; 5 | 6 | interface Props { 7 | data: Track[]; 8 | } 9 | 10 | const Tracklist: FC = ({ data }: Props) => { 11 | return ( 12 | 13 | {data.map((track: Track, idx: number) => { 14 | return ; 15 | })} 16 | 17 | ); 18 | }; 19 | 20 | export default Tracklist; 21 | -------------------------------------------------------------------------------- /frontend/components/Tracklist/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const StyledTrackCards = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | width: 100%; 9 | `; 10 | 11 | export default StyledTrackCards; 12 | -------------------------------------------------------------------------------- /frontend/event/index.ts: -------------------------------------------------------------------------------- 1 | import Collector from "./collector"; 2 | import Emitter from "./emitter"; 3 | 4 | export type { EventObject } from "./interface"; 5 | export { Collector, Emitter }; 6 | -------------------------------------------------------------------------------- /frontend/event/interface.ts: -------------------------------------------------------------------------------- 1 | export type ComponentEventType = 2 | | "click" 3 | | "dblclick" 4 | | "mouseover" 5 | | "mousemove" 6 | | "mousedown" 7 | | "mouseup" 8 | | "mouseout" 9 | | "reset" 10 | | "submit" 11 | | "dragstart" 12 | | "drag" 13 | | "dragend" 14 | | "dragenter" 15 | | "dragover" 16 | | "dragleave" 17 | | "drop"; 18 | 19 | export type EventType = ComponentEventType; 20 | 21 | export interface SimpleEvent { 22 | event_id: number; 23 | event_type: EventType[]; 24 | description?: string; 25 | stopPropagation?: boolean; 26 | } 27 | 28 | export interface ComplexEvent { 29 | event_id: number; 30 | description?: string; 31 | timer: number; 32 | sequence: string[]; 33 | } 34 | 35 | export interface EventObject { 36 | simple?: { [identifier: string]: SimpleEvent }; 37 | complex?: { [identifier: string]: ComplexEvent }; 38 | } 39 | -------------------------------------------------------------------------------- /frontend/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | interface Artist { 2 | id: number; 3 | artistName: string; 4 | cover: string; 5 | } 6 | 7 | interface Album { 8 | id: number; 9 | albumName: string; 10 | description?: string; 11 | cover: string; 12 | artistId: number; 13 | } 14 | 15 | interface Track { 16 | id: number; 17 | trackName: string; 18 | albumTrackNumber: number; 19 | albumId: number; 20 | Albums: Album; 21 | Artists: Artist[]; 22 | Liked: boolean; 23 | } 24 | 25 | interface Playlist { 26 | id: number; 27 | playlistName: string; 28 | description?: string; 29 | cover?: string; 30 | author: number; 31 | } 32 | 33 | interface Magazine { 34 | id: number; 35 | magazineName: string; 36 | magazineType: string; 37 | description: string; 38 | createdAt: number; 39 | playlistId: number; 40 | } 41 | 42 | interface News { 43 | id: number; 44 | newsName: string; 45 | type?: string; 46 | description?: string; 47 | playlistId?: number; 48 | } 49 | 50 | export type { Track, Artist, Album, Playlist, Magazine, News }; 51 | -------------------------------------------------------------------------------- /frontend/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /frontend/next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | async redirects() { 3 | return [ 4 | { 5 | source: "/", 6 | destination: "/today", 7 | permanent: true, 8 | }, 9 | ]; 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /frontend/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Document, { Html, Head, Main, NextScript } from "next/document"; 3 | import { ServerStyleSheet } from "styled-components"; 4 | 5 | export default class MyDocument extends Document { 6 | static async getInitialProps(ctx: any) { 7 | const sheet = new ServerStyleSheet(); 8 | const originalRenderPage = ctx.renderPage; 9 | 10 | try { 11 | ctx.renderPage = () => 12 | originalRenderPage({ 13 | enhanceApp: (App: any) => (props: any) => sheet.collectStyles(), 14 | }); 15 | 16 | const initialProps = await Document.getInitialProps(ctx); 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 | 36 |
37 | 38 | 39 | 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /frontend/pages/about.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from "react"; 2 | import Link from "next/link"; 3 | 4 | const AboutPage = memo(() => { 5 | return ( 6 | <> 7 |

About

8 |

This is the about page

9 |

10 | 11 | Go home 12 | 13 |

14 | 15 | 16 | 17 | 18 | ); 19 | }); 20 | 21 | export default AboutPage; 22 | -------------------------------------------------------------------------------- /frontend/pages/albums/[pid].tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import DetailPage from "../../components/DetailPage"; 3 | import { Album } from "../../interfaces"; 4 | 5 | const AlbumPage: FC = ({ Albums }: any) => { 6 | return ; 7 | }; 8 | 9 | export default AlbumPage; 10 | 11 | export async function getServerSideProps({ params }: any): Promise { 12 | const apiUrl = process.env.API_URL; 13 | const apiPort = process.env.API_PORT; 14 | 15 | const res = await fetch(`${apiUrl}:${apiPort}/api/albums/${params.pid}`); 16 | const { Albums } = await res.json(); 17 | 18 | return { props: { Albums } }; 19 | } 20 | -------------------------------------------------------------------------------- /frontend/pages/artists/[pid].tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import DetailPage from "../../components/DetailPage"; 3 | import { Artist } from "../../interfaces"; 4 | 5 | const ArtistPage: FC = ({ Artists }: any) => { 6 | return ; 7 | }; 8 | 9 | export default ArtistPage; 10 | 11 | export async function getServerSideProps({ params }: any): Promise { 12 | const apiUrl = process.env.API_URL; 13 | const apiPort = process.env.API_PORT; 14 | 15 | const res = await fetch(`${apiUrl}:${apiPort}/api/artists/${params.pid}`); 16 | const { Artists } = await res.json(); 17 | 18 | return { props: { Artists } }; 19 | } 20 | -------------------------------------------------------------------------------- /frontend/pages/auth/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import { GetServerSideProps } from "next"; 3 | import React, { useEffect, useState } from "react"; 4 | 5 | interface Props { 6 | token: string; 7 | } 8 | 9 | const AuthPage: React.FC = ({ token }: Props) => { 10 | const router = useRouter(); 11 | const [isMount, setIsMount] = useState(false); 12 | 13 | useEffect(() => { 14 | setIsMount(true); 15 | }, []); 16 | 17 | if (!isMount) return <>Loading Login Data; 18 | 19 | localStorage.setItem("token", token); 20 | 21 | router.push("/"); 22 | return <>; 23 | }; 24 | 25 | export const getServerSideProps: GetServerSideProps = async (context) => { 26 | const { 27 | query: { code, state }, 28 | } = context; 29 | 30 | const apiUrl = process.env.API_URL; 31 | const apiPort = process.env.API_PORT; 32 | 33 | const loginApiUrl = `${apiUrl}:${apiPort}/api/auth/naver?code=${code}&state=${state}`; 34 | const response = await fetch(loginApiUrl); 35 | const { token } = await response.json(); 36 | 37 | return { 38 | props: { token }, 39 | }; 40 | }; 41 | 42 | export default AuthPage; 43 | -------------------------------------------------------------------------------- /frontend/pages/chart/index.tsx: -------------------------------------------------------------------------------- 1 | import { memo, useState, useEffect } from "react"; 2 | import styled from "styled-components"; 3 | import myAxios from "../../utils/myAxios"; 4 | import ChartSlider from "../../components/ChartSlider"; 5 | import { mockData } from "../../components/ChartSlider/index.stories"; 6 | import GenreContainer from "../../components/GenreContainer"; 7 | 8 | const StyledChartPageWrapper = styled.div` 9 | box-sizing: border-box; 10 | padding: 5rem 7rem; 11 | `; 12 | 13 | const StyledPagetitle = styled.div` 14 | font-size: 2rem; 15 | font-weight: bold; 16 | margin: 2rem 0rem 1rem 0rem; 17 | `; 18 | 19 | const ChartsPage = memo(() => { 20 | const [genreData, setGenreData] = useState([]); 21 | useEffect(() => { 22 | myAxios.get("/genres").then((response: any) => { 23 | const { 24 | data: { Genres }, 25 | } = response; 26 | setGenreData(Genres); 27 | }); 28 | }, []); 29 | 30 | return ( 31 | 32 | 차트 33 | 34 | 35 | 36 | 37 | ); 38 | }); 39 | 40 | export default ChartsPage; 41 | -------------------------------------------------------------------------------- /frontend/pages/magazines/[pid].tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import DetailPage from "../../components/DetailPage"; 3 | import { Magazine } from "../../interfaces"; 4 | 5 | const MagazinePage: FC = ({ Magazines }: any) => { 6 | return ; 7 | }; 8 | 9 | export default MagazinePage; 10 | 11 | export async function getServerSideProps({ params }: any): Promise { 12 | const apiUrl = process.env.API_URL; 13 | const apiPort = process.env.API_PORT; 14 | 15 | const res = await fetch(`${apiUrl}:${apiPort}/api/magazines/${params.pid}`); 16 | const { Magazines } = await res.json(); 17 | 18 | return { props: { Magazines } }; 19 | } 20 | -------------------------------------------------------------------------------- /frontend/pages/news/[pid].tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import styled from "styled-components"; 3 | import DetailPage from "../../components/DetailPage"; 4 | import { News } from "../../interfaces"; 5 | 6 | const NewsPage: FC = ({ NewsData }: any) => { 7 | return ; 8 | }; 9 | 10 | export default NewsPage; 11 | 12 | export async function getServerSideProps({ params }: any): Promise { 13 | const apiUrl = process.env.API_URL; 14 | const apiPort = process.env.API_PORT; 15 | 16 | const res = await fetch(`${apiUrl}:${apiPort}/api/news/${params.pid}`); 17 | const { NewsData } = await res.json(); 18 | 19 | return { props: { NewsData } }; 20 | } 21 | -------------------------------------------------------------------------------- /frontend/pages/playlists/[pid].tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import styled from "styled-components"; 3 | import DetailPage from "../../components/DetailPage"; 4 | import { Playlist } from "../../interfaces"; 5 | 6 | const PlaylistPage: FC = ({ Playlists }: any) => { 7 | return ; 8 | }; 9 | 10 | export default PlaylistPage; 11 | 12 | export async function getServerSideProps({ params }: any): Promise { 13 | const apiUrl = process.env.API_URL; 14 | const apiPort = process.env.API_PORT; 15 | 16 | const res = await fetch(`${apiUrl}:${apiPort}/api/playlists/${params.pid}`); 17 | const { Playlists } = await res.json(); 18 | 19 | return { props: { Playlists } }; 20 | } 21 | -------------------------------------------------------------------------------- /frontend/pages/search/albums/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import myAxios from "../../../utils/myAxios"; 3 | import { 4 | StyledSearchAlbumPage, 5 | StyledResult, 6 | StyledResultText, 7 | StyledSearchAlbumCards, 8 | } from "./styled"; 9 | import SearchAlbumList from "../../../components/SearchSamples/SearchAlbumList"; 10 | 11 | const SearchAlbumPage = ({ filter }: { filter: string }): React.ReactElement => { 12 | const [sampleAlbums, setSampleAlbums] = useState([]); 13 | 14 | useEffect(() => { 15 | myAxios.get(`/search/albums?filter=${filter}&page=1`).then((response: any) => { 16 | const { data } = response; 17 | setSampleAlbums(data); 18 | }); 19 | }, [filter]); 20 | 21 | return ( 22 | 23 | 24 | {`'${filter}'의 검색 결과`} 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | }; 32 | 33 | SearchAlbumPage.getInitialProps = async ({ query }: { query?: { filter?: string } }) => { 34 | if (query && query.filter) { 35 | const { filter } = query; 36 | return { filter }; 37 | } 38 | }; 39 | 40 | export default SearchAlbumPage; 41 | -------------------------------------------------------------------------------- /frontend/pages/search/albums/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const StyledSearchAlbumPage = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | min-height: 85vh; 7 | margin: 1.75rem; 8 | box-sizing: border-box; 9 | padding-left: 5rem; 10 | padding-right: 10rem; 11 | width: 100%; 12 | `; 13 | 14 | const StyledResult = styled.div` 15 | display: flex; 16 | justify-content: space-between; 17 | align-items: center; 18 | height: 4rem; 19 | margin-bottom: 2rem; 20 | padding-bottom: 0.25rem; 21 | width: 100%; 22 | `; 23 | 24 | const StyledResultText = styled.div` 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | font-size: 1.25rem; 29 | font-weight: bold; 30 | `; 31 | 32 | const StyledSearchAlbumCards = styled.div` 33 | display: flex; 34 | `; 35 | 36 | export { StyledSearchAlbumPage, StyledResult, StyledResultText, StyledSearchAlbumCards }; 37 | export default StyledSearchAlbumPage; 38 | -------------------------------------------------------------------------------- /frontend/pages/search/artists/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const StyledSearchArtistPage = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | min-height: 85vh; 7 | margin: 1.75rem; 8 | box-sizing: border-box; 9 | padding-left: 5rem; 10 | padding-right: 10rem; 11 | width: 100%; 12 | `; 13 | 14 | const StyledResult = styled.div` 15 | display: flex; 16 | justify-content: space-between; 17 | align-items: center; 18 | height: 4rem; 19 | margin-bottom: 2rem; 20 | padding-bottom: 0.25rem; 21 | width: 100%; 22 | `; 23 | 24 | const StyledResultText = styled.div` 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | font-size: 1.25rem; 29 | font-weight: bold; 30 | `; 31 | 32 | const StyledSearchArtistList = styled.div` 33 | display: flex; 34 | `; 35 | 36 | export { StyledSearchArtistPage, StyledResult, StyledResultText, StyledSearchArtistList }; 37 | export default StyledSearchArtistPage; 38 | -------------------------------------------------------------------------------- /frontend/pages/search/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const StyledSearchPage = styled.div` 4 | display: flex; 5 | justify-content: center; 6 | flex-direction: column; 7 | margin: 5rem; 8 | `; 9 | 10 | export default StyledSearchPage; 11 | -------------------------------------------------------------------------------- /frontend/reduxModules/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import checkedTrackReducer from "./checkedTrack"; 3 | import playQueueReducer from "./playQueue"; 4 | 5 | export const rootReducer = combineReducers({ 6 | checkedTracks: checkedTrackReducer, 7 | playQueue: playQueueReducer, 8 | }); 9 | 10 | // eslint-disable-next-line no-undef 11 | export type RootState = ReturnType; 12 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "alwaysStrict": true, 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "isolatedModules": true, 8 | "jsx": "preserve", 9 | "lib": ["dom", "es2017"], 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "noEmit": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "noUnusedLocals": false, 15 | "noUnusedParameters": false, 16 | "resolveJsonModule": true, 17 | "skipLibCheck": true, 18 | "strict": true, 19 | "target": "esnext", 20 | "experimentalDecorators": true, 21 | "emitDecoratorMetadata": true, 22 | "outDir": "ts-out/", 23 | "types": [] 24 | }, 25 | "exclude": ["node_modules", "**/*.stories.tsx"], 26 | "include": ["**/*.ts", "**/*.tsx", "event/event.config.ts"] 27 | } 28 | -------------------------------------------------------------------------------- /frontend/utils/fetchLike.ts: -------------------------------------------------------------------------------- 1 | import myAxios from "./myAxios"; 2 | 3 | const fetchLike = async (trackId: number): Promise => { 4 | try { 5 | await myAxios.post("/tracks/like", { trackId }); 6 | return true; 7 | } catch (err) { 8 | return false; 9 | } 10 | }; 11 | 12 | const fetchUnlike = async (trackId: number): Promise => { 13 | try { 14 | await myAxios.post("/tracks/unlike", { trackId }); 15 | return true; 16 | } catch (err) { 17 | return false; 18 | } 19 | }; 20 | 21 | export { fetchLike, fetchUnlike }; 22 | -------------------------------------------------------------------------------- /frontend/utils/findTokenFromCookie.ts: -------------------------------------------------------------------------------- 1 | const findTokenFromCookie = (cookie: string): string | undefined => { 2 | if (!cookie) return; 3 | const cookieArr = cookie.split(";"); 4 | const tokenPart = cookieArr.find( 5 | (token) => token.includes("token") && !token.includes("csrftoken"), 6 | ); 7 | if (tokenPart) { 8 | return tokenPart.split("=")[1]; 9 | } 10 | }; 11 | 12 | export default findTokenFromCookie; 13 | -------------------------------------------------------------------------------- /frontend/utils/sendLog.ts: -------------------------------------------------------------------------------- 1 | import myAxios from "./myAxios"; 2 | 3 | const sendLog = (body: Object): void => { 4 | myAxios.post("/log/web", body); 5 | }; 6 | 7 | export default sendLog; 8 | -------------------------------------------------------------------------------- /iOS/MiniVibe/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | excluded: 2 | - Pods 3 | - MiniVibe/CoreData 4 | - MiniVibeTests 5 | - MiniVibeUITests 6 | disabled_rules: 7 | - trailing_whitespace 8 | - identifier_name 9 | -------------------------------------------------------------------------------- /iOS/MiniVibe/DiveEventCollector/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /iOS/MiniVibe/DiveEventCollector/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import DiveEventCollectorTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += DiveEventCollectorTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /iOS/MiniVibe/DiveEventCollector/Sources/DiveEventCollector/BaseEvent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseEvent.swift 3 | // MiniVibe 4 | // 5 | // Created by 류연수 on 2020/12/15. 6 | // 7 | 8 | import Foundation 9 | 10 | open class BaseEvent: Event { 11 | public var name: String 12 | public var createdAt: String? 13 | public var metadata: [String: String]? 14 | 15 | public init(name: String, createdAt: String?, metadata: [String: String]?) { 16 | self.name = name 17 | self.createdAt = createdAt 18 | self.metadata = metadata 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /iOS/MiniVibe/DiveEventCollector/Sources/DiveEventCollector/Event.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Event.swift 3 | // MiniVibe 4 | // 5 | // Created by 강병민 on 2020/12/06. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol Event: Codable { 11 | var name: String { get } 12 | var createdAt: String? { get } 13 | var metadata: [String: String]? { get } 14 | } 15 | -------------------------------------------------------------------------------- /iOS/MiniVibe/DiveEventCollector/Sources/DiveEventCollector/EventEngineProtocols.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnalyticsEngine.swift 3 | // TodoApp 4 | // 5 | // Created by 강병민 on 2020/12/06. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol EventSendableAndFetchable: EventSendable, EventFetchable { 11 | } 12 | 13 | public protocol EventSendable: class { 14 | func send(_ event: T) 15 | } 16 | 17 | public protocol EventFetchable: class { 18 | func fetch() -> [BaseEvent] 19 | } 20 | -------------------------------------------------------------------------------- /iOS/MiniVibe/DiveEventCollector/Sources/DiveEventCollector/MockServerEngine.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockAnalyticsEngine.swift 3 | // MiniVibe 4 | // 5 | // Created by 강병민 on 2020/12/15. 6 | // 7 | 8 | import Foundation 9 | 10 | public final class MockServerEngine: EventSendable { 11 | public init() { 12 | 13 | } 14 | public func send(_ event: T) { 15 | print("MockServer - \(event.name)") 16 | event.metadata?.forEach { key, value in 17 | print("ㄴ \(key) : \(value)") 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /iOS/MiniVibe/DiveEventCollector/Tests/DiveEventCollectorTests/DiveEventCollectorTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DiveEventCollector 3 | 4 | final class DiveEventCollectorTests: XCTestCase { 5 | func testEvent() { 6 | let event = BaseEvent(name: "testEvent", createdAt: nil, metadata: nil) 7 | XCTAssertEqual(event.name, "testEvent") 8 | // This is an example of a functional test case. 9 | // Use XCTAssert and related functions to verify your tests produce the correct 10 | // results. 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /iOS/MiniVibe/DiveEventCollector/Tests/DiveEventCollectorTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(DiveEventCollectorTests.allTests) 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /iOS/MiniVibe/DiveEventCollector/Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import DiveEventCollectorTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += DiveEventCollectorTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Reachability", 6 | "repositoryURL": "https://github.com/ashleymills/Reachability.swift", 7 | "state": { 8 | "branch": null, 9 | "revision": "c01bbdf2d633cf049ae1ed1a68a2020a8bda32e2", 10 | "version": "5.1.0" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024 – 3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024 – 3.png -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x – 3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x – 3.png -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x – 3-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x – 3-1.png -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x – 3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x – 3.png -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x – 3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x – 3.png -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x – 3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x – 3.png -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x – 3-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x – 3-1.png -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x – 3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x – 3.png -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x – 3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x – 3.png -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x – 3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x – 3.png -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x – 3-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x – 3-1.png -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x – 3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x – 3.png -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x – 3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x – 3.png -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x – 3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x – 3.png -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x – 3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x – 3.png -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x – 3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x – 3.png -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x – 3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x – 3.png -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x – 3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x – 3.png -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/Blueming.imageset/Blueming.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/Blueming.imageset/Blueming.jpg -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/Blueming.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Blueming.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/Dynamite.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Dynamite.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/Dynamite.imageset/Dynamite.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/Dynamite.imageset/Dynamite.jpg -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/FeelGood.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "FeelGood.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/FeelGood.imageset/FeelGood.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/FeelGood.imageset/FeelGood.jpg -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/Lovesick Girls.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Lovesick Girls.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/Lovesick Girls.imageset/Lovesick Girls.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/Lovesick Girls.imageset/Lovesick Girls.jpg -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/appIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "appIcon.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/appIcon.imageset/appIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/appIcon.imageset/appIcon.png -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/dj1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "dj1.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/dj1.imageset/dj1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/dj1.imageset/dj1.png -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/dj2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "dj2.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/dj2.imageset/dj2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/dj2.imageset/dj2.png -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/dj3.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "dj3.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/dj3.imageset/dj3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/dj3.imageset/dj3.png -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/dj4.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "dj4.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/dj4.imageset/dj4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/dj4.imageset/dj4.png -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/dj5.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "dj5.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/dj5.imageset/dj5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/dj5.imageset/dj5.png -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/dj6.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "dj6.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/dj6.imageset/dj6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/dj6.imageset/dj6.png -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/dj7.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "dj7.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/dj7.imageset/dj7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/dj7.imageset/dj7.png -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/dj8.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "dj8.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/dj8.imageset/dj8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/dj8.imageset/dj8.png -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/dj9.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "dj9.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/dj9.imageset/dj9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/dj9.imageset/dj9.png -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/favorite1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "favorite1.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/favorite1.imageset/favorite1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/favorite1.imageset/favorite1.png -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/favorite2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "favorite2.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/favorite2.imageset/favorite2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/favorite2.imageset/favorite2.png -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/favorite3.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "favorite3.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/favorite3.imageset/favorite3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/favorite3.imageset/favorite3.png -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/favorite4.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "favorite4.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/favorite4.imageset/favorite4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/favorite4.imageset/favorite4.jpg -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/favorite5.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "favorite5.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/favorite5.imageset/favorite5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/favorite5.imageset/favorite5.jpg -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "logo.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/logo.imageset/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/logo.imageset/logo.png -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/magazine1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "magazine1.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/magazine1.imageset/magazine1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/magazine1.imageset/magazine1.png -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/magazine2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "magazine2.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/magazine2.imageset/magazine2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/magazine2.imageset/magazine2.png -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/magazine3.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "magazine3.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/magazine3.imageset/magazine3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/magazine3.imageset/magazine3.png -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/magazine4.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "magazine4.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/magazine4.imageset/magazine4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/magazine4.imageset/magazine4.png -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/magazine5.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "magazine5.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/magazine5.imageset/magazine5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/magazine5.imageset/magazine5.png -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/recommend1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "recommend1.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/recommend1.imageset/recommend1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/recommend1.imageset/recommend1.png -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/recommend2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "recommend2.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/recommend2.imageset/recommend2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/recommend2.imageset/recommend2.png -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/recommend3.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "recommend3.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/recommend3.imageset/recommend3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/recommend3.imageset/recommend3.png -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/recommend4.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "recommend4.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/recommend4.imageset/recommend4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/recommend4.imageset/recommend4.png -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/recommend5.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "recommend5.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/recommend5.imageset/recommend5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-A-User-Event-Collector/a5eb041a65696b776ef535e15ef890b07b148b6e/iOS/MiniVibe/MiniVibe/Common/Assets.xcassets/images/recommend5.imageset/recommend5.png -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/CustomView/Accessory/DeleteAccessory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeleteAccessory.swift 3 | // MiniVibe 4 | // 5 | // Created by 류연수 on 2020/12/15. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct DeleteAccessory: View { 11 | let toggleDeleted: (() -> Void)? 12 | 13 | var body: some View { 14 | Button(action: { 15 | toggleDeleted?() 16 | }, label: { 17 | Image(systemName: "minus.circle") 18 | .accesoryModifier(color: .gray, size: .small) 19 | .padding([.leading, .vertical]) 20 | }) 21 | .buttonStyle(BorderlessButtonStyle()) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/CustomView/Accessory/DownloadAccessory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DownloadAccessory.swift 3 | // MiniVibe 4 | // 5 | // Created by 류연수 on 2020/12/08. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct DownloadAccessory: View { 11 | var isSavedToLibrary: Bool 12 | let toggleLiked: (() -> Void)? 13 | 14 | var body: some View { 15 | Button(action: { 16 | toggleLiked?() 17 | }, label: { 18 | Image(systemName: isSavedToLibrary ? "square.and.arrow.down.fill" : "square.and.arrow.down") 19 | .accesoryModifier(color: .gray, size: .medium) 20 | .padding([.leading, .vertical]) 21 | }) 22 | .buttonStyle(BorderlessButtonStyle()) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/CustomView/Accessory/EllipsisAccessory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EllipsisAccessory.swift 3 | // MiniVibe 4 | // 5 | // Created by 류연수 on 2020/12/08. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct EllipsisAccessory: View { 11 | var body: some View { 12 | Button(action: { 13 | print("show menu") 14 | }, label: { 15 | Image(systemName: "ellipsis") 16 | .accesoryModifier(color: .gray, size: .small) 17 | .padding([.leading, .vertical]) 18 | }) 19 | .buttonStyle(BorderlessButtonStyle()) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/CustomView/BasicRowCellView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BasicRowCellView.swift 3 | // MiniVibe 4 | // 5 | // Created by 류연수 on 2020/12/03. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct BasicRowCellView: View { 11 | let title: String 12 | let subTitle: String? 13 | let coverURLString: String? 14 | let coverData: Data? 15 | 16 | var body: some View { 17 | HStack { 18 | URLImage(urlString: coverURLString, imageData: coverData) 19 | .frame(width: 44, height: 44, alignment: .center) 20 | .padding(.vertical, 2) 21 | VStack(alignment: .leading) { 22 | Text(title) 23 | .modifier(Title2()) 24 | if let subTitle = subTitle { 25 | Text(subTitle) 26 | .modifier(Description2()) 27 | } 28 | } 29 | Spacer() 30 | } 31 | } 32 | } 33 | 34 | struct TrackInfoView_Previews: PreviewProvider { 35 | static var previews: some View { 36 | BasicRowCellView(title: "title", subTitle: "artist", coverURLString: nil, coverData: nil) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/CustomView/MemorySafeNavigationLink.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MemorySafeNavigationLink.swift 3 | // MiniVibe 4 | // 5 | // Created by 류연수 on 2020/12/05. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct MemorySafeNavigationLink: View { 11 | let contentView: Content 12 | let destination: AnyView 13 | 14 | var body: some View { 15 | ZStack { 16 | contentView 17 | NavigationLink( 18 | destination: LazyView(destination), 19 | label: { 20 | Rectangle().hidden() 21 | } 22 | ) 23 | } 24 | } 25 | } 26 | 27 | struct LazyView: View { 28 | let build: () -> Content 29 | init(_ build: @autoclosure @escaping () -> Content) { 30 | self.build = build 31 | } 32 | var body: Content { 33 | build() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/CustomView/SwappableRowView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwappableRowView.swift 3 | // MiniVibe 4 | // 5 | // Created by 강병민 on 2020/12/11. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SwappableRowView: View { 11 | let title: String 12 | let subTitle: String? 13 | let coverURLString: String? 14 | let coverData: Data? 15 | 16 | var body: some View { 17 | HStack { 18 | SwappableImageWithURL(coverURLString, data: coverData) 19 | .frame(width: 44, height: 44, alignment: .center) 20 | .padding(.vertical, 2) 21 | VStack(alignment: .leading) { 22 | Text(title) 23 | .modifier(Title2()) 24 | if let subTitle = subTitle { 25 | Text(subTitle) 26 | .modifier(Description2()) 27 | } 28 | } 29 | Spacer() 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Extension/Data+PrettifyJSONString.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Data+PrettifyJSONString.swift 3 | // MiniVibe 4 | // 5 | // Created by 류연수 on 2020/12/11. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Data { 11 | var prettifyJSONString: NSString? { 12 | guard let object = try? JSONSerialization.jsonObject(with: self, options: []), 13 | let data = try? JSONSerialization.data(withJSONObject: object, options: [.prettyPrinted]), 14 | let prettifyJSONString = NSString(data: data, encoding: String.Encoding.utf8.rawValue) else { return nil } 15 | 16 | return prettifyJSONString 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Extension/Date+StringForWeb.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Date+StringForWeb.swift 3 | // MiniVibe 4 | // 5 | // Created by 류연수 on 2020/12/12. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Date { 11 | 12 | public func convertToStringForWeb(dateFormat: String = "yyyy-MM-dd'T'HH:mm:ss.SSSZ") -> String { 13 | let dateFormatter = DateFormatter() 14 | dateFormatter.locale = Locale(identifier: "ko") 15 | dateFormatter.dateFormat = dateFormat 16 | return dateFormatter.string(from: self) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Extension/Image+accesoryModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Image+accesoryModifier.swift 3 | // MiniVibe 4 | // 5 | // Created by 강병민 on 2020/11/28. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension Image { 11 | 12 | enum AccessorySize: CGFloat { 13 | case small = 20, medium = 24, large = 40 14 | } 15 | 16 | func fitModifier(size: CGFloat = UIScreen.main.bounds.width) -> some View { 17 | self 18 | .resizable() 19 | .aspectRatio(contentMode: .fit) 20 | .frame(width: size - 40, height: size - 40, alignment: .center) 21 | } 22 | 23 | func accesoryModifier(color: Color, size: AccessorySize) -> some View { 24 | self 25 | .resizable() 26 | .aspectRatio(contentMode: .fit) 27 | .frame(width: size.rawValue, height: size.rawValue, alignment: .center) 28 | .accentColor(color) 29 | } 30 | 31 | func resizeToFit(padding amount: CGFloat) -> some View { 32 | self 33 | .resizable() 34 | .scaledToFit() 35 | .padding(.all, amount) 36 | } 37 | 38 | func base() -> some View { 39 | self 40 | .resizable() 41 | .aspectRatio(contentMode: .fit) 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Extension/Rectangle+Modifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Rectangle+Modifier.swift 3 | // MiniVibe 4 | // 5 | // Created by 강병민 on 2020/12/02. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension Rectangle { 11 | func clearBottom() -> some View { 12 | self 13 | .fill(Color.clear) 14 | .frame(height: 50) 15 | .listRowInsets(EdgeInsets()) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Extension/Sequence+indexed.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Sequence+indexed.swift 3 | // MiniVibe 4 | // 5 | // Created by 류연수 on 2020/11/25. 6 | // 7 | 8 | import Foundation 9 | 10 | //extension Sequence { 11 | // func indexed() -> [ (offset: Int, element: Element) ] { 12 | // return Array(enumerated()) 13 | // } 14 | //} 15 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Image/ImageCache.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageCache.swift 3 | // MiniVibe 4 | // 5 | // Created by 류연수 on 2020/12/08. 6 | // 7 | 8 | import Foundation 9 | 10 | final class ImageCache: CacheService { 11 | static let shared = ImageCache() 12 | private override init() { 13 | super.init() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Image/SwappableImageWithURL.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwappableImageWithURL.swift 3 | // MiniVibe 4 | // 5 | // Created by 강병민 on 2020/12/11. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SwappableImageWithURL: View { 11 | 12 | @ObservedObject var imageLoader: SwappableImageLoaderAndCache 13 | 14 | init(_ url: String?, data: Data?) { 15 | imageLoader = SwappableImageLoaderAndCache(imageURL: url, data: data) 16 | } 17 | 18 | var body: some View { 19 | if let defaultImage = UIImage(named: "appIcon") { 20 | Image(uiImage: UIImage(data: imageLoader.imageData) ?? defaultImage) 21 | .resizable() 22 | .scaledToFit() 23 | } 24 | 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Network/CacheService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CacheService.swift 3 | // MiniVibe 4 | // 5 | // Created by 류연수 on 2020/12/08. 6 | // 7 | 8 | import Foundation 9 | 10 | class CacheService { 11 | var cache = NSCache() 12 | 13 | func get(forKey: String) -> T? { 14 | return cache.object(forKey: NSString(string: forKey)) 15 | } 16 | 17 | func set(forKey: String, data: T) { 18 | cache.setObject(data, forKey: NSString(string: forKey)) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Network/NetworkManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkManager.swift 3 | // MiniVibe 4 | // 5 | // Created by 류연수 on 2020/12/02. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | class NetworkManager { 12 | 13 | private let network = NetworkService(session: URLSession.shared) 14 | private var cancellables = Set() 15 | 16 | func request(urlRequest: URLRequest?, 17 | completion: @escaping (Data) -> Void) { 18 | guard let urlRequest = urlRequest else { return } 19 | 20 | network.request(request: urlRequest) 21 | .sink { result in 22 | switch result { 23 | case .failure(let error): 24 | print(error) 25 | case .finished: 26 | break 27 | } 28 | } receiveValue: { data in 29 | completion(data) 30 | } 31 | .store(in: &cancellables) 32 | } 33 | 34 | deinit { 35 | cancellables.forEach { $0.cancel() } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Network/NetworkService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkService.swift 3 | // MiniVibe 4 | // 5 | // Created by 류연수 on 2020/11/30. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | final class NetworkService { 12 | 13 | enum NetworkError: Error { 14 | case invalidRequest 15 | case unknownError(message: String) 16 | } 17 | 18 | private let session: URLSession 19 | 20 | init(session: URLSession) { 21 | self.session = session 22 | } 23 | 24 | func request(request: URLRequest) -> AnyPublisher { 25 | return session.dataTaskPublisher(for: request) 26 | .tryMap { data, response -> Data in 27 | guard let httpResponse = response as? HTTPURLResponse, 28 | (200...299).contains(httpResponse.statusCode) else { 29 | throw NetworkError.invalidRequest 30 | } 31 | return data 32 | } 33 | .mapError { error -> NetworkError in 34 | .unknownError(message: error.localizedDescription) 35 | } 36 | .eraseToAnyPublisher() 37 | } 38 | 39 | deinit { 40 | session.invalidateAndCancel() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Network/RequestBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestBuilder.swift 3 | // MiniVibe 4 | // 5 | // Created by 강병민 on 2020/12/01. 6 | // 7 | 8 | import Foundation 9 | 10 | enum NetworkMethod: String { 11 | case get 12 | case post 13 | case put 14 | case patch 15 | case delete 16 | } 17 | 18 | struct RequestBuilder { 19 | private let url: URL? 20 | private let method: NetworkMethod 21 | private let body: Data? 22 | private let headers: [String: String] 23 | 24 | init(url: URL?, 25 | method: NetworkMethod, 26 | body: Data? = nil, 27 | headers: [String: String] = ["Content-Type": "application/json"]) { 28 | 29 | self.url = url 30 | self.method = method 31 | self.body = body 32 | self.headers = headers 33 | } 34 | 35 | func create() -> URLRequest? { 36 | guard let url = url else { return nil } 37 | var request = URLRequest(url: url) 38 | request.httpMethod = method.rawValue.uppercased() 39 | if let body = body { 40 | request.httpBody = body 41 | } 42 | request.allHTTPHeaderFields = headers 43 | return request 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Router/RouterProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RouterProtocol.swift 3 | // MiniVibe 4 | // 5 | // Created by 류연수 on 2020/11/25. 6 | // 7 | 8 | import SwiftUI 9 | 10 | protocol RoutingTypeProtocol { 11 | 12 | } 13 | 14 | protocol StarterOrientedRouterProtocol { 15 | associatedtype RoutingStarter 16 | func getDestination(id: Int) -> AnyView 17 | } 18 | 19 | protocol DestinationOrientedRouterProtocol { 20 | associatedtype RoutingType 21 | func getDestination(to routingDestination: RoutingType, with: Int?) -> AnyView 22 | } 23 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/Test/DeallocPrinter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeallocPrinter.swift 3 | // MiniVibe 4 | // 5 | // Created by 류연수 on 2020/12/14. 6 | // 7 | 8 | import Foundation 9 | 10 | class DeallocPrinter { 11 | let target: String 12 | 13 | init(target: String) { 14 | print("\(target) DeallocPrinter init") 15 | self.target = target 16 | } 17 | 18 | deinit { 19 | print("\(target) deinit") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/ViewModifier/ButtonStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ButtonStyle.swift 3 | // MiniVibe 4 | // 5 | // Created by 류연수 on 2020/11/28. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct TrackListButtonStyle: ViewModifier { 11 | func body(content: Content) -> some View { 12 | content 13 | .padding() 14 | .frame(maxWidth: .infinity) 15 | .foregroundColor(.primary) 16 | .background(Color(UIColor.systemGray6)) 17 | .cornerRadius(6.0) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Common/ViewModifier/NavigationBarStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigationBarStyle.swift 3 | // MiniVibe 4 | // 5 | // Created by 류연수 on 2020/11/25. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct NavigationBarStyle: ViewModifier { 11 | let title: String 12 | 13 | func body(content: Content) -> some View { 14 | content 15 | .navigationTitle(title) 16 | .navigationBarTitleDisplayMode(.inline) 17 | // .foregroundColor(.primary) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/CoreData/API/CoreDataAPIManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreDataAPIManager.swift 3 | // MiniVibe 4 | // 5 | // Created by 류연수 on 2020/12/10. 6 | // 7 | 8 | import Foundation 9 | import CoreData 10 | 11 | class CoreDataAPIManager { 12 | let persistenceController = PersistenceController.shared 13 | let context: NSManagedObjectContext 14 | 15 | init() { 16 | context = persistenceController.context 17 | } 18 | 19 | func save() { 20 | let result = persistenceController.save() 21 | processResult(result: result) 22 | } 23 | 24 | func processResult(result: PersistenceController.CoreDataResult) { 25 | switch result { 26 | case .success(.saveSuccess): 27 | print("saveSuccess") 28 | case .success(.deleteSuccess): 29 | print("deleteSuccess") 30 | case .success(.deleteAllSuccess): 31 | print("deleteAllSuccess") 32 | case .failure(.saveFailed): 33 | print("saveFailed") 34 | case .failure(.deleteFailed): 35 | print("deleteFailed") 36 | case .failure(.deleteAllFailed): 37 | print("deleteAllFailed") 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/CoreData/MiniVibe.xcdatamodeld/.xccurrentversion: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | _XCCurrentVersionName 6 | MiniVibe v3.xcdatamodel 7 | 8 | 9 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/MiniVibeApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MiniVibeApp.swift 3 | // MiniVibe 4 | // 5 | // Created by 류연수 on 2020/11/20. 6 | // 7 | 8 | import SwiftUI 9 | import DiveEventCollector 10 | 11 | @main 12 | struct MiniVibeApp: App { 13 | let manager = EventManager(serverEngine: MongoDBEventEngine(), 14 | backupEngine: BackupEventEngine(), 15 | alertEngine: AlertEventEngine()) 16 | var body: some Scene { 17 | WindowGroup { 18 | TabBarView(manager: manager) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/MiniVibeEventCollector/Engines/BackupEventEngine.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BackupEventEngine.swift 3 | // MiniVibe 4 | // 5 | // Created by 류연수 on 2020/12/14. 6 | // 7 | 8 | import Foundation 9 | import DiveEventCollector 10 | 11 | class BackupEventEngine: EventSendableAndFetchable { 12 | 13 | let coreEventManager = CoreEventAPI() 14 | 15 | func send(_ event: T) { 16 | coreEventManager.create(with: event) 17 | } 18 | 19 | func fetch() -> [BaseEvent] { 20 | let events = coreEventManager.fetchAll() 21 | coreEventManager.deleteAll() 22 | return events 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/MiniVibeEventCollector/Engines/MongoDBEventEngine.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MongoDBEventEngine.swift 3 | // MiniVibe 4 | // 5 | // Created by 강병민 on 2020/12/10. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | import DiveEventCollector 11 | 12 | class MongoDBEventEngine: EventSendable { 13 | 14 | private let networkManager = NetworkManager() 15 | private var cancellables = Set() 16 | 17 | func send(_ event: T) { 18 | post(event) 19 | } 20 | 21 | func post(_ event: T) { 22 | let url = URLBuilder(pathType: .api, 23 | endPoint: .log).create() 24 | let jsonBody = try? JSONEncoder().encode(event) 25 | let urlrequest = RequestBuilder(url: url, 26 | method: .post, 27 | body: jsonBody).create() 28 | 29 | networkManager.request(urlRequest: urlrequest) { _ in } 30 | } 31 | 32 | deinit { 33 | cancellables.forEach { $0.cancel() } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/MiniVibeEventCollector/Events/ButtonEvent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ButtonEvent.swift 3 | // MiniVibe 4 | // 5 | // Created by 강병민 on 2020/12/09. 6 | // 7 | 8 | import Foundation 9 | import DiveEventCollector 10 | 11 | struct ButtonEvent: Event { 12 | var name: String 13 | var createdAt: String? 14 | var metadata: [String: String]? 15 | 16 | private init(name: String, metadata: [String: String]? = nil) { 17 | self.name = name 18 | self.createdAt = Date().convertToStringForWeb() 19 | self.metadata = metadata 20 | } 21 | 22 | static let magazineTouched = ButtonEvent(name: "magazineTouched") 23 | static let genreTouched = ButtonEvent(name: "genreTouched") 24 | static let newsTouched = ButtonEvent(name: "newsTouched") 25 | static let djStationTouched = ButtonEvent(name: "djStationTouched") 26 | 27 | } 28 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/MiniVibeEventCollector/Views/LoggableButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoggableButton.swift 3 | // MiniVibe 4 | // 5 | // Created by 강병민 on 2020/12/09. 6 | // 7 | 8 | import SwiftUI 9 | import DiveEventCollector 10 | 11 | struct LoggableButton: View { 12 | let content : () -> Content 13 | let manager: EventManager 14 | let event: T 15 | 16 | init(@ViewBuilder content: @escaping () -> Content, manager: EventManager, event: T) { 17 | self.content = content 18 | self.manager = manager 19 | self.event = event 20 | } 21 | 22 | var body: some View { 23 | Button(action: { [weak manager = manager] in 24 | manager?.log(event) 25 | }, label: { 26 | content() 27 | }) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Models/Album.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Album.swift 3 | // MiniVibe 4 | // 5 | // Created by 강병민 on 2020/11/30. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Album: Codable, Identifiable, Cellable { 11 | let id: Int? 12 | var name: String 13 | let description: String? 14 | let cover: String? 15 | let coverData: Data? 16 | let artistID: Int? 17 | let artist: Artist? 18 | 19 | enum CodingKeys: String, CodingKey { 20 | case id, description, coverData 21 | case name = "albumName" 22 | case cover 23 | case artistID = "artistId" 24 | case artist = "Artists" 25 | } 26 | } 27 | 28 | extension Album { 29 | 30 | init(from coreAlbum: CoreAlbum) { 31 | self.id = Int(coreAlbum.id) 32 | self.name = coreAlbum.name ?? "" 33 | self.description = coreAlbum.descript ?? "" 34 | self.cover = nil 35 | self.coverData = coreAlbum.cover 36 | self.artistID = nil 37 | self.artist = nil 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Models/Artist.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Artist.swift 3 | // MiniVibe 4 | // 5 | // Created by 강병민 on 2020/11/30. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Artist: Codable, Identifiable, Cellable { 11 | let id: Int? 12 | var name: String 13 | let cover: String? 14 | let coverData: Data? 15 | 16 | enum CodingKeys: String, CodingKey { 17 | case id 18 | case name = "artistName" 19 | case cover 20 | case coverData 21 | } 22 | } 23 | 24 | extension Artist { 25 | 26 | init(from coreArtist: CoreArtist) { 27 | self.id = Int(coreArtist.id) 28 | self.name = coreArtist.name ?? "" 29 | self.cover = nil 30 | self.coverData = coreArtist.cover 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Models/DJStation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DJStation.swift 3 | // MiniVibe 4 | // 5 | // Created by 강병민 on 2020/11/25. 6 | // 7 | 8 | import Foundation 9 | 10 | struct DJStation: Codable, Identifiable { 11 | let id: Int 12 | let stationName: String 13 | let cover: String 14 | let popularity: Int 15 | } 16 | 17 | struct DJStationResponse: Codable { 18 | let djStations: [DJStation] 19 | 20 | enum CodingKeys: String, CodingKey { 21 | case djStations = "DJStations" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Models/Magazine.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Magazine.swift 3 | // MiniVibe 4 | // 5 | // Created by 강병민 on 2020/12/01. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol Thumbnailable { 11 | var id: Int { get set } 12 | var name: String { get set } 13 | var description: String { get set } 14 | var cover: String? { get set } 15 | } 16 | 17 | struct Magazine: Thumbnailable, Codable { 18 | var id: Int 19 | var name, description: String 20 | var cover: String? 21 | let playlistID: Int 22 | let tracks: [Track]? 23 | let createdAt, type: String 24 | enum CodingKeys: String, CodingKey { 25 | case id 26 | case name = "magazineName" 27 | case type = "magazineType" 28 | case cover 29 | case description 30 | case createdAt 31 | case playlistID = "playlistId" 32 | case tracks = "Tracks" 33 | } 34 | } 35 | 36 | // MARK: - API response를 위한 모델 37 | struct MagazineReponse: Codable { 38 | let magazine: Magazine 39 | enum CodingKeys: String, CodingKey { 40 | case magazine = "Magazines" 41 | } 42 | } 43 | 44 | struct Magazines: Codable { 45 | let magazines: [Magazine] 46 | enum CodingKeys: String, CodingKey { 47 | case magazines = "Magazines" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Models/Playlist.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Playlist.swift 3 | // MiniVibe 4 | // 5 | // Created by 강병민 on 2020/11/30. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Playlist: Thumbnailable, Codable { 11 | var id: Int 12 | var name, description: String 13 | var cover: String? 14 | let author: Int 15 | let user: User? 16 | let tracks: [Track]? 17 | 18 | enum CodingKeys: String, CodingKey { 19 | case id, description, cover, author 20 | case name = "playlistName" 21 | case tracks = "Tracks" 22 | case user = "users" 23 | } 24 | } 25 | 26 | // MARK: - API response를 위한 모델 27 | struct PlayListReponse: Codable { 28 | let playlist: Playlist 29 | 30 | enum CodingKeys: String, CodingKey { 31 | case playlist = "Playlists" 32 | } 33 | } 34 | 35 | struct Playlists: Codable { 36 | let playlists: [Playlist] 37 | enum CodingKeys: String, CodingKey { 38 | case playlists = "Playlists" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Models/Search.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Search.swift 3 | // MiniVibe 4 | // 5 | // Created by 류연수 on 2020/12/03. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Search: Codable { 11 | let albums: [Album]? 12 | let tracks: [Track]? 13 | let artists: [Artist]? 14 | 15 | enum CodingKeys: String, CodingKey { 16 | case albums = "Albums" 17 | case tracks = "Tracks" 18 | case artists = "Artists" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Models/User.swift: -------------------------------------------------------------------------------- 1 | // 2 | // User.swift 3 | // MiniVibe 4 | // 5 | // Created by 강병민 on 2020/11/30. 6 | // 7 | 8 | import Foundation 9 | 10 | struct User: Codable { 11 | let name: String 12 | 13 | enum CodingKeys: String, CodingKey { 14 | case name = "userName" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Scenes/DJStation/DJStationListViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DJStationListViewModel.swift 3 | // MiniVibe 4 | // 5 | // Created by 강병민 on 2020/11/26. 6 | // 7 | import Foundation 8 | import Combine 9 | 10 | class DJStationListViewModel: ObservableObject { 11 | @Published var stations = [DJStation]() 12 | 13 | private let networkManager = NetworkManager() 14 | 15 | func fetch() { 16 | let url = URLBuilder(pathType: .api, 17 | endPoint: .djStations).create() 18 | let urlRequest = RequestBuilder(url: url, 19 | method: .get).create() 20 | networkManager.request(urlRequest: urlRequest) { [weak self] data in 21 | if let decodedData = try? JSONDecoder().decode(DJStationResponse.self, from: data) { 22 | DispatchQueue.main.async { [weak self] in 23 | self?.stations = decodedData.djStations 24 | } 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Scenes/Error/ErrorView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ErrorView.swift 3 | // MiniVibe 4 | // 5 | // Created by 강병민 on 2020/12/01. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ErrorView: View { 11 | var body: some View { 12 | Text("404 Not Found") 13 | } 14 | } 15 | 16 | struct ErrorView_Previews: PreviewProvider { 17 | static var previews: some View { 18 | ErrorView() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Scenes/Library/LibraryViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LibraryViewModel.swift 3 | // MiniVibe 4 | // 5 | // Created by 강병민 on 2020/12/08. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | import DiveEventCollector 11 | 12 | class LibraryViewModel: ObservableObject { 13 | @Published var tracks = [Track]() 14 | 15 | private let manager: EventManager 16 | private let coreDataManager = CoreTrackAPI() 17 | 18 | init(manager: EventManager) { 19 | self.manager = manager 20 | } 21 | 22 | func fetch() { 23 | var tracks = [Track]() 24 | let predicate = NSPredicate(format: "isSavedToLibrary Contains %d", true) 25 | let coreTracks = coreDataManager.fetch(predicate: predicate) 26 | coreTracks.forEach { coreTrack in 27 | tracks.append(Track(from: coreTrack)) 28 | } 29 | self.tracks = tracks 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Scenes/Magazine/MagazineViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MagazineViewModel.swift 3 | // MiniVibe 4 | // 5 | // Created by 류연수 on 2020/12/02. 6 | // 7 | 8 | import Foundation 9 | 10 | class MagazineViewModel: ObservableObject { 11 | @Published var magazine: Magazine? 12 | 13 | private let networkManager = NetworkManager() 14 | 15 | func fetch(id: Int) { 16 | let url = URLBuilder(pathType: .api, 17 | endPoint: .magazines, 18 | id: id).create() 19 | let urlRequest = RequestBuilder(url: url, 20 | method: .get).create() 21 | networkManager.request(urlRequest: urlRequest) { [weak self] data in 22 | if let decodedData = try? JSONDecoder().decode(MagazineReponse.self, from: data) { 23 | DispatchQueue.main.async { [weak self] in 24 | self?.magazine = decodedData.magazine 25 | } 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Scenes/NowPlaying/BlurView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BlurView.swift 3 | // MiniVibe 4 | // 5 | // Created by 강병민 on 2020/11/28. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct BlurView: UIViewRepresentable { 11 | var style: UIBlurEffect.Style = .systemChromeMaterial 12 | 13 | func makeUIView(context: Context) -> UIVisualEffectView { 14 | return UIVisualEffectView(effect: UIBlurEffect(style: style)) 15 | } 16 | 17 | func updateUIView(_ uiView: UIVisualEffectView, context: Context) { 18 | uiView.effect = UIBlurEffect(style: style) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Scenes/Player/SubViews/PlayerHeaderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlayerHeaderView.swift 3 | // MiniVibe 4 | // 5 | // Created by 강병민 on 2020/12/07. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct PlayerHeaderView: View { 11 | @Binding var showMediaPlayer: Bool 12 | 13 | var body: some View { 14 | HStack { 15 | Image(systemName: "flame") 16 | .accesoryModifier(color: .secondary, size: .medium) 17 | .hidden() 18 | Spacer() 19 | Text("플레이리스트 제목") 20 | Spacer() 21 | Button(action: { 22 | showMediaPlayer.toggle() 23 | }, label: { 24 | Image(systemName: "chevron.down") 25 | .accesoryModifier(color: .secondary, size: .medium) 26 | }) 27 | .accessibility(identifier: "ClosePlayer") 28 | } 29 | } 30 | } 31 | 32 | struct PlayerHeaderView_Previews: PreviewProvider { 33 | static var previews: some View { 34 | PlayerHeaderView(showMediaPlayer: .constant(true)) 35 | .preferredColorScheme(.dark) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Scenes/Player/SubViews/SwipableImageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwipableImageView.swift 3 | // MiniVibe 4 | // 5 | // Created by 강병민 on 2020/12/15. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SwipableImageView: View { 11 | @State private var offset: CGSize = .zero 12 | 13 | let urlString: String? 14 | let coverData: Data? 15 | var didSwipeLeft: (() -> Void)? 16 | var didSwipeRight: (() -> Void)? 17 | 18 | var body: some View { 19 | SwappableImageWithURL(urlString, data: coverData) 20 | .padding() 21 | .highPriorityGesture( 22 | DragGesture(minimumDistance: 20, coordinateSpace: .local) 23 | .onChanged { gesture in 24 | offset = gesture.translation 25 | } 26 | .onEnded { _ in 27 | if offset.width > 30 { 28 | didSwipeRight?() 29 | } 30 | if offset.width < -30 { 31 | didSwipeLeft?() 32 | } 33 | } 34 | ) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Scenes/Player/SubViews/ToggleableImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ToggleableImage.swift 3 | // MiniVibe 4 | // 5 | // Created by 강병민 on 2020/12/07. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ToggleableImage: View { 11 | @Binding var isEnabled: Bool 12 | let imageWhenTrue: String 13 | let colorWhenTrue: Color 14 | let imageWhenFalse: String 15 | let colorWhenFalse: Color 16 | let size: Image.AccessorySize 17 | var eventHandler: (() -> Void)? 18 | 19 | var body: some View { 20 | Button(action: { 21 | isEnabled.toggle() 22 | eventHandler?() 23 | }, label: { 24 | if isEnabled { 25 | Image(systemName: imageWhenTrue) 26 | .accesoryModifier(color: colorWhenTrue, size: size) 27 | .padding(5) 28 | } else { 29 | Image(systemName: imageWhenFalse) 30 | .accesoryModifier(color: colorWhenFalse, size: size) 31 | .padding(5) 32 | } 33 | }) 34 | .accentColor(.primary) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Scenes/Playlist/PlaylistCellView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlaylistCellView.swift 3 | // MiniVibe 4 | // 5 | // Created by 류연수 on 2020/11/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct PlaylistCellView: View { 11 | @Binding var playlist: Playlist 12 | 13 | var body: some View { 14 | HStack(spacing: 20) { 15 | Image(playlist.cover ?? "logo") 16 | .resizable() 17 | .frame(width: 90, height: 90) 18 | VStack(alignment: .leading, spacing: 5) { 19 | Text(playlist.name) 20 | .modifier(Title2()) 21 | if let description = playlist.description { 22 | Text(description) 23 | .modifier(Description2()) 24 | .padding(.bottom, 2) 25 | } 26 | // if let date = playlist.createdAt { 27 | // Text(date) 28 | // .modifier(Description2()) 29 | // } 30 | if let author = playlist.user.username { 31 | Text(author) 32 | .modifier(Description2()) 33 | } 34 | } 35 | }.padding(.top) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Scenes/Playlist/PlaylistRouter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlaylistRouter.swift 3 | // MiniVibe 4 | // 5 | // Created by 류연수 on 2020/11/25. 6 | // 7 | 8 | import SwiftUI 9 | 10 | enum PlaylistRoutingType: RoutingTypeProtocol { 11 | case magazines 12 | case recommended 13 | case favorites 14 | } 15 | 16 | class PlaylistRouter: StarterOrientedRouterProtocol { 17 | typealias RoutingStarter = PlaylistRoutingType 18 | 19 | let routingStarter: RoutingStarter 20 | 21 | init(routingStarter: RoutingStarter) { 22 | self.routingStarter = routingStarter 23 | } 24 | 25 | func getDestination() -> AnyView { 26 | switch routingStarter { 27 | case .magazines: 28 | return AnyView(MagazineView()) 29 | case .recommended: 30 | return AnyView(PlaylistView()) 31 | case .favorites: 32 | return AnyView(PlaylistView()) 33 | } 34 | } 35 | 36 | func title() -> String { 37 | switch routingStarter { 38 | case .magazines: 39 | return "VIBE MAG" 40 | case .recommended: 41 | return "VIBE 추천 플레이리스트" 42 | case .favorites: 43 | return "즐겨듣는 플레이리스트" 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Scenes/Playlist/PlaylistViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlaylistViewModel.swift 3 | // MiniVibe 4 | // 5 | // Created by 강병민 on 2020/12/01. 6 | // 7 | 8 | import Foundation 9 | 10 | class PlaylistViewModel: ObservableObject { 11 | @Published var playlist: Playlist? 12 | 13 | private let network = NetworkManager() 14 | 15 | func fetch(id: Int) { 16 | let url = URLBuilder(pathType: .api, 17 | endPoint: .playlists, 18 | id: id).create() 19 | let urlRequest = RequestBuilder(url: url, 20 | method: .get).create() 21 | network.request(urlRequest: urlRequest) { [weak self] data in 22 | if let decodedData = try? JSONDecoder().decode(PlayListReponse.self, from: data) { 23 | DispatchQueue.main.async { [weak self] in 24 | self?.playlist = decodedData.playlist 25 | } 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Scenes/Search/SubViews/GenreCellView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GenreCellView.swift 3 | // MiniVibe 4 | // 5 | // Created by 류연수 on 2020/12/01. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct GenreCellView: View { 11 | let title: String 12 | var didTouchCell: (() -> Void)? 13 | 14 | var body: some View { 15 | Button(action: { 16 | didTouchCell?() 17 | }, label: { 18 | HStack { 19 | RoundedRectangle(cornerRadius: 25, style: .continuous) 20 | .frame(width: 6, height: 30) 21 | .foregroundColor(.blue) 22 | Text(title) 23 | .modifier(Title2()) 24 | Spacer() 25 | }.modifier(TrackListButtonStyle()) 26 | }) 27 | } 28 | } 29 | 30 | struct GenreCellView_Previews: PreviewProvider { 31 | static var previews: some View { 32 | GenreCellView(title: "Title") 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Scenes/Search/SubViews/RectangleCellInfoView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RectangleCellInfoView.swift 3 | // MiniVibe 4 | // 5 | // Created by 류연수 on 2020/12/01. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct RectangleCellInfoView: View { 11 | var body: some View { 12 | ZStack(alignment: .topLeading) { 13 | Rectangle() 14 | .fill(Color.white) 15 | HStack { 16 | Text("BoostCamp 01_A 팀 사용자 이벤트 수집기 DIVE 선보여...") 17 | .multilineTextAlignment(.leading) 18 | .font(.system(size: 18, weight: .bold)) 19 | .foregroundColor(.black) 20 | .lineLimit(2) 21 | .padding([.horizontal, .top]) 22 | Spacer() 23 | } 24 | } 25 | .frame(width: UIScreen.main.bounds.width - 20, 26 | height: UIScreen.main.bounds.width/4) 27 | } 28 | } 29 | 30 | struct RectangleCellInfoView_Previews: PreviewProvider { 31 | static var previews: some View { 32 | RectangleCellInfoView() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Scenes/Search/SubViews/RectangleCellView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RectangleSlideCellView.swift 3 | // MiniVibe 4 | // 5 | // Created by 류연수 on 2020/12/01. 6 | // 7 | 8 | import SwiftUI 9 | import DiveEventCollector 10 | 11 | struct RectangleCellView: View { 12 | private let manager: EventManager 13 | init(manager: EventManager) { 14 | self.manager = manager 15 | } 16 | 17 | var body: some View { 18 | Button(action: { [weak manager = self.manager] in 19 | manager?.log(ButtonEvent.newsTouched) 20 | }, label: { 21 | ZStack(alignment: .bottomLeading) { 22 | Image("logo") 23 | .resizable() 24 | .scaledToFill() 25 | .frame(width: UIScreen.main.bounds.width - 20, height: UIScreen.main.bounds.width/2) 26 | // RectangleCellInfoView() 27 | } 28 | }) 29 | } 30 | } 31 | 32 | struct RectangleCellView_Previews: PreviewProvider { 33 | static var previews: some View { 34 | RectangleCellView(manager: EventManager(serverEngine: nil, backupEngine: nil, alertEngine: nil)) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Scenes/Search/SubViews/RectangleListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RectangleListView.swift 3 | // MiniVibe 4 | // 5 | // Created by 류연수 on 2020/12/01. 6 | // 7 | 8 | import SwiftUI 9 | import DiveEventCollector 10 | 11 | struct RectangleListView: View { 12 | private let manager: EventManager 13 | init(manager: EventManager) { 14 | self.manager = manager 15 | } 16 | 17 | private let layout = [GridItem(.flexible())] 18 | 19 | var body: some View { 20 | ScrollView(.horizontal, showsIndicators: false) { 21 | LazyHGrid(rows: layout, 22 | spacing: 20) { 23 | RectangleCellView(manager: manager) 24 | RectangleCellView(manager: manager) 25 | RectangleCellView(manager: manager) 26 | } 27 | } 28 | } 29 | } 30 | 31 | struct RectangleListView_Previews: PreviewProvider { 32 | static var previews: some View { 33 | RectangleListView(manager: EventManager(serverEngine: nil, backupEngine: nil, alertEngine: nil)) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Scenes/Search/SubViews/SearchAfterView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchAfterView.swift 3 | // MiniVibe 4 | // 5 | // Created by 류연수 on 2020/12/02. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SearchAfterView: View { 11 | @ObservedObject var viewModel: SearchViewModel 12 | 13 | var body: some View { 14 | SearchAfterCategoryView(type: .track, cellDatas: viewModel.tracks) 15 | SearchAfterCategoryView(type: .album, cellDatas: viewModel.albums) 16 | SearchAfterCategoryView(type: .artist, cellDatas: viewModel.artists) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Scenes/Search/SubViews/SearchBeforeView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchBefore.swift 3 | // MiniVibe 4 | // 5 | // Created by 강병민 on 2020/12/05. 6 | // 7 | 8 | import SwiftUI 9 | import DiveEventCollector 10 | 11 | struct SearchBeforeView: View { 12 | private let manager: EventManager 13 | init(manager: EventManager) { 14 | self.manager = manager 15 | } 16 | var body: some View { 17 | VStack { 18 | RectangleListView(manager: manager) 19 | Spacer() 20 | HStack { 21 | Text("장르") 22 | .modifier(Title1()) 23 | Spacer() 24 | } 25 | GenreListView(manager: manager) 26 | } 27 | } 28 | } 29 | 30 | struct SearchBefore_Previews: PreviewProvider { 31 | static var previews: some View { 32 | SearchBeforeView(manager: EventManager(serverEngine: nil, backupEngine: nil, alertEngine: nil)) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Scenes/TabBar/CustomTabViewContent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomTabViewContent.swift 3 | // MiniVibe 4 | // 5 | // Created by 강병민 on 2020/12/14. 6 | // 7 | 8 | import SwiftUI 9 | import DiveEventCollector 10 | 11 | struct CustomTabViewContent: View { 12 | private let manager: EventManager 13 | private var selectedTab: MiniVibeTab 14 | 15 | init(manager: EventManager, selectedTab: MiniVibeTab) { 16 | self.manager = manager 17 | self.selectedTab = selectedTab 18 | } 19 | 20 | var body: some View { 21 | if case MiniVibeTab.today = selectedTab { 22 | TodayView(manager: manager) 23 | } else if case MiniVibeTab.chart = selectedTab { 24 | ChartView(playlistID: 18, manager: manager) 25 | } else if case MiniVibeTab.search = selectedTab { 26 | SearchView(manager: manager) 27 | } else if case MiniVibeTab.library = selectedTab { 28 | LibraryView(manager: manager) 29 | } 30 | 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Scenes/TabBar/TabIconView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabIconView.swift 3 | // MiniVibe 4 | // 5 | // Created by 강병민 on 2020/12/07. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct TabIconView: View { 11 | let height: CGFloat 12 | let imageName: String 13 | let labelText: String 14 | let tab: MiniVibeTab 15 | var iconHeight: CGFloat { 16 | height * 0.3 17 | } 18 | 19 | @Binding var selectedTab: MiniVibeTab 20 | 21 | var body: some View { 22 | VStack(alignment: .center) { 23 | Image(systemName: imageName) 24 | .resizable() 25 | .aspectRatio(contentMode: .fit) 26 | .foregroundColor(selectedTab == tab ? .red : .gray) 27 | .frame(width: iconHeight, height: iconHeight) 28 | .padding(.top, 20) 29 | .padding(.horizontal, 15) 30 | if let labelText = labelText { 31 | Text(labelText) 32 | .padding(.bottom, 5) 33 | .foregroundColor(selectedTab == tab ? .red : .gray) 34 | .modifier(Description1()) 35 | } 36 | } 37 | .onTapGesture { 38 | selectedTab = tab 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Scenes/Today/SubViews/CategoryHeaderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CategoryHeaderView.swift 3 | // MiniVibe 4 | // 5 | // Created by 강병민 on 2020/11/25. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct CategoryHeaderView: View { 11 | let title: String 12 | var body: some View { 13 | HStack(alignment: .firstTextBaseline) { 14 | Text(title) 15 | .font(.headline) 16 | .padding(.top, 5) 17 | Spacer() 18 | Text("더보기") 19 | .font(.subheadline) 20 | .padding(.top, 5) 21 | } 22 | .padding(.vertical, 10) 23 | } 24 | } 25 | 26 | struct CategoryInfoView_Previews: PreviewProvider { 27 | static var previews: some View { 28 | CategoryHeaderView(title: "hi") 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Scenes/Today/SubViews/CategoryItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CategoryCell.swift 3 | // MiniVibe 4 | // 5 | // Created by 강병민 on 2020/11/25. 6 | // 7 | 8 | import Foundation 9 | 10 | struct CategoryItem: Identifiable { 11 | let id: Int 12 | let imageName: String? 13 | let title: String? 14 | let author: String? 15 | let description: String? 16 | } 17 | 18 | extension CategoryItem { 19 | init(magazine: Magazine) { 20 | id = magazine.id 21 | imageName = magazine.cover 22 | title = nil 23 | author = nil 24 | description = nil 25 | } 26 | } 27 | 28 | extension CategoryItem { 29 | init(playlist: Playlist, type: MiniVibeType) { 30 | id = playlist.id 31 | imageName = playlist.cover 32 | title = playlist.name 33 | author = playlist.user?.name 34 | switch type { 35 | case .recommendations: 36 | description = playlist.description 37 | default: 38 | description = nil 39 | } 40 | } 41 | } 42 | 43 | extension CategoryItem { 44 | init(station: DJStation) { 45 | id = station.id 46 | imageName = station.cover 47 | title = nil 48 | author = nil 49 | description = nil 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Scenes/TrackList/SubViews/TrackListButtonView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TrackListButtonView.swift 3 | // MiniVibe 4 | // 5 | // Created by 류연수 on 2020/11/28. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct TrackListButtonView: View { 11 | var didPressPlayButton: (() -> Void)? 12 | var didPressShuffleButton: (() -> Void)? 13 | 14 | var body: some View { 15 | HStack { 16 | Button(action: { 17 | didPressPlayButton?() 18 | }, label: { 19 | HStack { 20 | Image(systemName: "play.fill") 21 | Text("PLAY") 22 | .modifier(Title2()) 23 | }.modifier(TrackListButtonStyle()) 24 | }) 25 | Button(action: { 26 | didPressShuffleButton?() 27 | }, label: { 28 | HStack { 29 | Image(systemName: "shuffle") 30 | Text("SHUFFLE") 31 | .modifier(Title2()) 32 | }.modifier(TrackListButtonStyle()) 33 | }) 34 | } 35 | } 36 | } 37 | 38 | struct TrackListButtonView_Previews: PreviewProvider { 39 | static var previews: some View { 40 | TrackListButtonView() 41 | .preferredColorScheme(.dark) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibe/Scenes/TrackList/TrackListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TrackListView.swift 3 | // DemoTrackView 4 | // 5 | // Created by 강병민 on 2020/11/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct TrackListView: View { 11 | @EnvironmentObject var nowPlayingViewModel: PlayerViewModel 12 | private let layout = [GridItem(.flexible())] 13 | private let tracks: [Track] 14 | 15 | init(tracks: [Track]) { 16 | self.tracks = tracks 17 | } 18 | 19 | var body: some View { 20 | let headerView = TrackListButtonView( 21 | didPressPlayButton: { [weak nowPlayingViewModel = self.nowPlayingViewModel] in 22 | nowPlayingViewModel?.update(with: tracks) 23 | }, 24 | didPressShuffleButton: { [weak nowPlayingViewModel = self.nowPlayingViewModel] in 25 | nowPlayingViewModel?.update(with: tracks, isShuffled: true) 26 | } 27 | ) 28 | Section(header: headerView) { 29 | LazyVGrid(columns: layout) { 30 | ForEach(tracks) { track -> TrackCellView in 31 | TrackCellView(hasHeartAccessory: true, track: track) 32 | } 33 | Rectangle() 34 | .clearBottom() 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibeTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibeTests/MiniVibeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MiniVibeTests.swift 3 | // MiniVibeTests 4 | // 5 | // Created by 류연수 on 2020/11/20. 6 | // 7 | 8 | import XCTest 9 | import Combine 10 | @testable import MiniVibe 11 | 12 | class MiniVibeTests: XCTestCase { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibeUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /iOS/MiniVibe/MiniVibeUITests/XCUIElement+Scroll.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCUIElement+Scroll.swift 3 | // MiniVibeUITests 4 | // 5 | // Created by 강병민 on 2020/12/15. 6 | // 7 | 8 | import XCTest 9 | 10 | extension XCUIElement { 11 | func scrollToElement(element: XCUIElement) { 12 | while !element.visible() { 13 | swipeUp() 14 | } 15 | } 16 | 17 | func visible() -> Bool { 18 | guard self.exists && !self.frame.isEmpty else { return false } 19 | return XCUIApplication().windows.element(boundBy: 0).frame.contains(self.frame) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/MiniVibe/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | target 'MiniVibe' do 5 | # Comment the next line if you don't want to use dynamic frameworks 6 | use_frameworks! 7 | 8 | # Pods for MiniVibe 9 | pod 'SwiftLint' 10 | target 'MiniVibeTests' do 11 | inherit! :search_paths 12 | # Pods for testing 13 | end 14 | 15 | target 'MiniVibeUITests' do 16 | # Pods for testing 17 | end 18 | 19 | end 20 | -------------------------------------------------------------------------------- /iOS/MiniVibe/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - SwiftLint (0.40.3) 3 | 4 | DEPENDENCIES: 5 | - SwiftLint 6 | 7 | SPEC REPOS: 8 | trunk: 9 | - SwiftLint 10 | 11 | SPEC CHECKSUMS: 12 | SwiftLint: dfd554ff0dff17288ee574814ccdd5cea85d76f7 13 | 14 | PODFILE CHECKSUM: bf87d05083fb0538cec02bb7b828a4aa0e264b11 15 | 16 | COCOAPODS: 1.9.3 17 | --------------------------------------------------------------------------------