├── .editorconfig ├── .gitattributes ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── 기능-구현.md │ └── 문제-사항-보고.md ├── pull_request_template.md └── workflows │ ├── docker-image.yml │ └── scripts │ └── deploy.sh ├── .gitignore ├── .prettierrc ├── .yarn └── releases │ └── yarn-4.5.1.cjs ├── .yarnrc.yml ├── README.md ├── client ├── .gitignore ├── .storybook │ ├── main.ts │ └── preview.ts ├── Dockerfile ├── README.md ├── index.html ├── package.json ├── postcss.config.js ├── public │ ├── favicon.ico │ ├── mockServiceWorker.js │ └── vite.svg ├── src │ ├── App.tsx │ ├── GlobalBoundary.tsx │ ├── Layout.tsx │ ├── NetworkBoundary.tsx │ ├── app │ │ └── router │ │ │ ├── ProtectedRoute.tsx │ │ │ ├── Router.tsx │ │ │ ├── config.ts │ │ │ ├── index.ts │ │ │ └── routes.tsx │ ├── assets │ │ ├── default-banner.webp │ │ ├── github-logo.png │ │ ├── logo-album-cover.png │ │ ├── logo-album-cover.webp │ │ └── react.svg │ ├── entities │ │ ├── album │ │ │ └── types.ts │ │ ├── albumRegister │ │ │ └── types.ts │ │ ├── comment │ │ │ └── types.ts │ │ ├── message │ │ │ └── types.ts │ │ ├── room │ │ │ └── types.ts │ │ └── songDetail │ │ │ ├── index.ts │ │ │ └── ui │ │ │ ├── LyricsPanel.css │ │ │ ├── LyricsPanel.tsx │ │ │ ├── PlaylistPanel.css │ │ │ ├── PlaylistPanel.tsx │ │ │ ├── PlaylistPanelCredit.tsx │ │ │ └── PlaylistPanelSong.tsx │ ├── features │ │ ├── albumRegister │ │ │ ├── index.ts │ │ │ ├── model │ │ │ │ └── useAlbumForm.ts │ │ │ └── ui │ │ │ │ ├── AlbumForm.tsx │ │ │ │ └── SongForm.tsx │ │ ├── albumStreaming │ │ │ ├── hook │ │ │ │ ├── constants.ts │ │ │ │ └── useStreamingPlayer.ts │ │ │ ├── index.ts │ │ │ └── model │ │ │ │ └── types.ts │ │ ├── chattingInput │ │ │ ├── index.ts │ │ │ ├── model │ │ │ │ └── sendMessage.ts │ │ │ └── ui │ │ │ │ └── ChatInput.tsx │ │ ├── commentInput │ │ │ ├── index.ts │ │ │ └── ui │ │ │ │ └── CommentInput.tsx │ │ ├── songDetail │ │ │ ├── index.ts │ │ │ ├── model │ │ │ │ └── types.ts │ │ │ └── ui │ │ │ │ ├── CategoryButton.tsx │ │ │ │ ├── SongDetail.tsx │ │ │ │ ├── SongDetailContent.tsx │ │ │ │ └── SongDetailHeader.tsx │ │ └── useCounter │ │ │ ├── index.ts │ │ │ └── ui │ │ │ └── UserCounter.tsx │ ├── index.css │ ├── main.tsx │ ├── pages │ │ ├── AdminLoginPage │ │ │ ├── index.ts │ │ │ └── ui │ │ │ │ └── AdminLoginPage.tsx │ │ ├── AdminPage │ │ │ ├── index.ts │ │ │ └── ui │ │ │ │ └── AdminPage.tsx │ │ ├── AlbumPage │ │ │ ├── index.ts │ │ │ └── ui │ │ │ │ └── AlbumPage.tsx │ │ ├── MainPage │ │ │ ├── index.ts │ │ │ └── ui │ │ │ │ └── MainPage.tsx │ │ ├── StreamingErrorPage │ │ │ ├── index.ts │ │ │ └── ui │ │ │ │ └── StreamingErrorPage.tsx │ │ └── StreamingPage │ │ │ ├── index.ts │ │ │ └── ui │ │ │ ├── Notice.tsx │ │ │ ├── Standby.tsx │ │ │ └── StreamingPage.tsx │ ├── reset.css │ ├── shared │ │ ├── api │ │ │ ├── adminAPI.ts │ │ │ ├── errorMessage.ts │ │ │ ├── publicAPI.ts │ │ │ └── socket.ts │ │ ├── hook │ │ │ ├── useSocketEvents.ts │ │ │ └── useStreamingRoom.ts │ │ ├── icon │ │ │ ├── ChevronDown.tsx │ │ │ ├── InearLogo.tsx │ │ │ ├── Person.tsx │ │ │ ├── PlayIcon.tsx │ │ │ ├── SendIcon.tsx │ │ │ ├── Volume.tsx │ │ │ └── VolumeMuted.tsx │ │ ├── lottie │ │ │ └── music.json │ │ ├── store │ │ │ ├── state │ │ │ │ ├── chatState.ts │ │ │ │ ├── socketState.ts │ │ │ │ └── voteState.ts │ │ │ ├── useChatMessageStore.ts │ │ │ ├── useSocketStore.ts │ │ │ ├── useVoteStore.ts │ │ │ └── utils │ │ │ │ └── socketEvents.ts │ │ ├── ui │ │ │ ├── Button.tsx │ │ │ ├── Input.tsx │ │ │ ├── Timer.tsx │ │ │ └── index.ts │ │ └── util │ │ │ └── timeUtils.ts │ ├── vite-env.d.ts │ └── widgets │ │ ├── albums │ │ ├── index.ts │ │ └── ui │ │ │ ├── AlbumArtist.tsx │ │ │ ├── AlbumCard.tsx │ │ │ ├── AlbumList.tsx │ │ │ ├── Comment.tsx │ │ │ ├── CommentList.tsx │ │ │ ├── Playlist.tsx │ │ │ ├── Scrollbar.css │ │ │ └── TrackItem.tsx │ │ ├── banner │ │ ├── index.ts │ │ └── ui │ │ │ ├── Banner.css │ │ │ ├── Banner.tsx │ │ │ └── BannerSlide.tsx │ │ ├── chatting │ │ ├── hook │ │ │ └── useChatMessage.ts │ │ ├── index.ts │ │ ├── ui │ │ │ ├── Chatting.css │ │ │ ├── Chatting.tsx │ │ │ ├── ChattingContainer.tsx │ │ │ ├── Message.tsx │ │ │ └── MessageList.tsx │ │ └── useChatMessage.ts │ │ ├── sidebar │ │ ├── hook │ │ │ └── useSidebarAlbum.ts │ │ ├── index.ts │ │ └── ui │ │ │ ├── Credit.tsx │ │ │ ├── RoomList.tsx │ │ │ ├── RoomListItem.tsx │ │ │ ├── Sidebar.tsx │ │ │ └── StreamingList.css │ │ ├── streaming │ │ ├── index.ts │ │ └── ui │ │ │ ├── AlbumBackground.tsx │ │ │ ├── AlbumInfo.tsx │ │ │ ├── AudioController.tsx │ │ │ ├── Streaming.tsx │ │ │ └── Volume.css │ │ └── vote │ │ ├── index.ts │ │ ├── ui │ │ ├── ScrollBar.css │ │ └── Vote.tsx │ │ └── useVote.ts ├── tailwind.config.js ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── docker-compose.yml ├── nginx ├── Dockerfile ├── conf.d │ └── default.conf └── nginx.conf ├── package.json ├── server ├── .editorconfig ├── .gitattributes ├── .gitignore ├── .vscode │ ├── extensions.json │ └── settings.json ├── .yarn │ └── install-state.gz ├── Dockerfile ├── README.md ├── nest-cli.json ├── package-lock.json ├── package.json ├── public │ ├── hls-test.html │ └── socket-test.html ├── socket-test.yml ├── src │ ├── admin │ │ ├── admin.controller.ts │ │ ├── admin.guard.ts │ │ ├── admin.module.ts │ │ ├── admin.redis.repository.ts │ │ ├── admin.service.ts │ │ ├── admin.transaction.service.ts │ │ └── dto │ │ │ ├── album.dto.ts │ │ │ └── song.dto.ts │ ├── album │ │ ├── album.controller.ts │ │ ├── album.entity.ts │ │ ├── album.module.ts │ │ ├── album.redis.repository.ts │ │ ├── album.repository.ts │ │ ├── album.service.ts │ │ └── dto │ │ │ ├── album-detail-response.dto.ts │ │ │ ├── album-response.dto.ts │ │ │ ├── ended-album-response.dto.ts │ │ │ ├── main-banner-response.dto.ts │ │ │ └── side-bar-response.dto.ts │ ├── app.controller.ts │ ├── app.module.ts │ ├── comment │ │ ├── comment.controller.ts │ │ ├── comment.entity.ts │ │ ├── comment.module.ts │ │ ├── comment.repository.ts │ │ ├── comment.service.ts │ │ └── dto │ │ │ └── album-comment-response.dto.ts │ ├── common │ │ ├── common.module.ts │ │ ├── constants │ │ │ └── repository.constant.ts │ │ ├── exceptions │ │ │ ├── base.exception.ts │ │ │ ├── domain │ │ │ │ ├── album │ │ │ │ │ ├── album-creation-fail.exception.ts │ │ │ │ │ ├── album-not-found-by-timestamp.exception.ts │ │ │ │ │ └── album-not-found.exception.ts │ │ │ │ ├── room │ │ │ │ │ ├── room-inactive.exception.ts │ │ │ │ │ ├── room-is-full.exception.ts │ │ │ │ │ ├── room-not-found.exception.ts │ │ │ │ │ ├── user-not-in-room.exception.ts │ │ │ │ │ └── user-room-info-not-found.exception.ts │ │ │ │ ├── song │ │ │ │ │ └── missing-song-files.exception.ts │ │ │ │ └── vote │ │ │ │ │ └── already-vote-this-room.exception.ts │ │ │ ├── global-exception.filter.ts │ │ │ └── ws-exception.filter.ts │ │ ├── logger │ │ │ ├── logger-context.middleware.ts │ │ │ └── logger.config.ts │ │ ├── randomname │ │ │ └── random-name.util.ts │ │ ├── redis │ │ │ ├── redis.module.ts │ │ │ └── redis.provider.ts │ │ ├── s3Cache │ │ │ └── s3Cache.service.ts │ │ └── scheduler │ │ │ └── scheduler.service.ts │ ├── emoji │ │ ├── dto │ │ │ └── emoji-request.dto.ts │ │ ├── emoji.controller.ts │ │ ├── emoji.module.ts │ │ └── emoji.service.ts │ ├── main.ts │ ├── music │ │ ├── music.controller.ts │ │ ├── music.module.ts │ │ ├── music.processor.ts │ │ ├── music.repository.ts │ │ ├── music.service.ts │ │ └── parser │ │ │ └── m3u8-parser.ts │ ├── room │ │ ├── room.constant.ts │ │ ├── room.controller.ts │ │ ├── room.entity.ts │ │ ├── room.gateway.ts │ │ ├── room.module.ts │ │ ├── room.repository.ts │ │ └── room.service.ts │ └── song │ │ ├── dto │ │ ├── song-response.dto.ts │ │ └── song-save.dto.ts │ │ ├── song.entity.ts │ │ ├── song.module.ts │ │ └── song.repository.ts ├── test │ ├── admin.integration.spec.ts │ ├── config │ │ └── typeorm.config.ts │ ├── jest-e2e.json │ └── mock │ │ └── album.mock.ts ├── tsconfig.build.json └── tsconfig.json ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.{js,json,yml}] 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | /.yarn/** linguist-vendored 2 | /.yarn/releases/* binary 3 | /.yarn/plugins/**/* binary 4 | /.pnp.* binary linguist-generated 5 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @rdyjun @Kontae @yoonseo-han @chaeryeon823 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/기능-구현.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 기능 구현 3 | about: 구현한(할) 기능에 대한 이슈 4 | title: "[BE/FE] 구현" 5 | labels: "\U0001F680 feature" 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## ✨ 기능에 대한 기대 11 | 12 | ## 🔗 관련 링크 13 | 14 | ## 📋 구현/작업 목록 15 | 16 | ## 📆 구현 계획 17 | 18 | ## ✅ 결과 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/문제-사항-보고.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 문제 사항 보고 3 | about: 발생한 문제에 대한 정리 4 | title: "[BUG] 문제" 5 | labels: "\U0001F47D bug" 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## 🐞 문제 사항 11 | 12 | ## 🔗 원인 13 | 14 | ## 📋 해결 과정 15 | 16 | ## ✅ 결과 17 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## 📋개요 2 | 3 | ## 🕰️예상 리뷰시간 4 | 5 | ## 📢상세내용 6 | 7 | ## 💥특이사항 -------------------------------------------------------------------------------- /.github/workflows/scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # nginx 설정 업데이트 및 리로드 함수 4 | update_and_reload_nginx() { 5 | local target=$1 6 | echo "Updating nginx configuration for $target environment..." 7 | 8 | # 임시 설정 파일 생성 9 | cat > nginx/conf.d/default.conf << EOF 10 | upstream backend { 11 | server server-$target:3000; 12 | } 13 | 14 | server { 15 | listen 80; 16 | server_name localhost; 17 | 18 | location / { 19 | proxy_pass http://client:5173; 20 | proxy_http_version 1.1; 21 | proxy_set_header Upgrade \$http_upgrade; 22 | proxy_set_header Connection 'upgrade'; 23 | proxy_set_header Host \$host; 24 | } 25 | 26 | location /api { 27 | proxy_pass http://backend; 28 | proxy_http_version 1.1; 29 | proxy_set_header Host \$host; 30 | proxy_set_header Upgrade \$http_upgrade; 31 | proxy_set_header Connection 'upgrade'; 32 | } 33 | } 34 | EOF 35 | 36 | # nginx 컨테이너 재시작 37 | echo "Restarting nginx to apply new configuration..." 38 | docker restart nginx 39 | 40 | # 설정이 제대로 적용되었는지 확인 41 | sleep 2 42 | docker exec nginx nginx -t 43 | } 44 | 45 | # Blue에서 Green으로 전환 46 | switch_to_green() { 47 | echo "Deploying Green environment..." 48 | 49 | cd 50 | cd web18-inear 51 | 52 | # Green 환경 시작 53 | docker compose -f docker-compose-green.yml up -d 54 | 55 | # nginx 설정 업데이트 및 리로드 56 | update_and_reload_nginx "green" 57 | 58 | echo "Stopping Blue server..." 59 | if docker ps -q --filter name=server-blue > /dev/null; then 60 | docker compose -f docker-compose-blue.yml stop server-blue 61 | docker compose -f docker-compose-blue.yml rm -f server-blue 62 | fi 63 | } 64 | # Green에서 Blue로 전환 65 | switch_to_blue() { 66 | echo "Deploying Blue environment..." 67 | 68 | cd 69 | cd web18-inear 70 | 71 | # Blue 환경 시작 72 | docker compose -f docker-compose-blue.yml up -d 73 | 74 | # nginx 설정 업데이트 및 리로드 75 | update_and_reload_nginx "blue" 76 | 77 | echo "Stopping Green server..." 78 | if docker ps -q --filter name=server-green > /dev/null; then 79 | docker compose -f docker-compose-green.yml stop server-green 80 | docker compose -f docker-compose-green.yml rm -f server-green 81 | fi 82 | } 83 | 84 | # 현재 실행 중인 환경 확인 및 전환 85 | if [ -n "$(docker ps -q --filter name=blue)" ]; then 86 | switch_to_green 87 | echo "switch_to_green 실행" 88 | else 89 | switch_to_blue 90 | echo "switch_to_blue 실행" 91 | fi -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | server/node_modules 14 | 15 | eslint.config.js 16 | 17 | server/.yarn 18 | 19 | server/music* 20 | 21 | package-lock.json 22 | 23 | # Runtime data 24 | pids 25 | *.pid 26 | *.seed 27 | *.pid.lock 28 | 29 | # Directory for instrumented libs generated by jscoverage/JSCover 30 | lib-cov 31 | 32 | # Coverage directory used by tools like istanbul 33 | coverage 34 | *.lcov 35 | 36 | # nyc test coverage 37 | .nyc_output 38 | 39 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 40 | .grunt 41 | 42 | # Bower dependency directory (https://bower.io/) 43 | bower_components 44 | 45 | # node-waf configuration 46 | .lock-wscript 47 | 48 | # Compiled binary addons (https://nodejs.org/api/addons.html) 49 | build/Release 50 | 51 | # Dependency directories 52 | node_modules/ 53 | jspm_packages/ 54 | 55 | # Snowpack dependency directory (https://snowpack.dev/) 56 | web_modules/ 57 | 58 | # TypeScript cache 59 | *.tsbuildinfo 60 | 61 | # Optional npm cache directory 62 | .npm 63 | 64 | # Optional eslint cache 65 | .eslintcache 66 | 67 | # Optional stylelint cache 68 | .stylelintcache 69 | 70 | # Microbundle cache 71 | .rpt2_cache/ 72 | .rts2_cache_cjs/ 73 | .rts2_cache_es/ 74 | .rts2_cache_umd/ 75 | 76 | # Optional REPL history 77 | .node_repl_history 78 | 79 | # Output of 'npm pack' 80 | *.tgz 81 | 82 | # Yarn Integrity file 83 | .yarn-integrity 84 | 85 | # dotenv environment variable files 86 | .env 87 | .env.development.local 88 | .env.test.local 89 | .env.production.local 90 | .env.local 91 | 92 | # parcel-bundler cache (https://parceljs.org/) 93 | .cache 94 | .parcel-cache 95 | 96 | # Next.js build output 97 | .next 98 | out 99 | 100 | # Nuxt.js build / generate output 101 | .nuxt 102 | dist 103 | 104 | # Gatsby files 105 | .cache/ 106 | # Comment in the public line in if your project uses Gatsby and not Next.js 107 | # https://nextjs.org/blog/next-9-1#public-directory-support 108 | # public 109 | 110 | # vuepress build output 111 | .vuepress/dist 112 | 113 | # vuepress v2.x temp and cache directory 114 | .temp 115 | .cache 116 | 117 | # Docusaurus cache and generated files 118 | .docusaurus 119 | 120 | # Serverless directories 121 | .serverless/ 122 | 123 | # FuseBox cache 124 | .fusebox/ 125 | 126 | # DynamoDB Local files 127 | .dynamodb/ 128 | 129 | # TernJS port file 130 | .tern-port 131 | 132 | # Stores VSCode versions used for testing VSCode extensions 133 | .vscode-test 134 | 135 | # yarn v2 136 | .yarn/cache 137 | .yarn/unplugged 138 | .yarn/build-state.yml 139 | .yarn/install-state.gz 140 | .pnp.* 141 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "arrowParents": "avoid", 4 | "singleQuote": true, 5 | "trailingComma": "all", 6 | "tabWidth": 2, 7 | "semi": true, 8 | "useTabs": false 9 | } 10 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | enableGlobalCache: true 2 | 3 | nodeLinker: node-modules 4 | 5 | yarnPath: .yarn/releases/yarn-4.5.1.cjs 6 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | *storybook.log 27 | src/stories/* 28 | 29 | .env.development 30 | .env.production -------------------------------------------------------------------------------- /client/.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from '@storybook/react-vite'; 2 | 3 | import { join, dirname } from 'path'; 4 | 5 | /** 6 | * This function is used to resolve the absolute path of a package. 7 | * It is needed in projects that use Yarn PnP or are set up within a monorepo. 8 | */ 9 | function getAbsolutePath(value: string): any { 10 | return dirname(require.resolve(join(value, 'package.json'))); 11 | } 12 | const config: StorybookConfig = { 13 | stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], 14 | addons: [ 15 | getAbsolutePath('@storybook/addon-onboarding'), 16 | getAbsolutePath('@storybook/addon-essentials'), 17 | getAbsolutePath('@chromatic-com/storybook'), 18 | getAbsolutePath('@storybook/addon-interactions'), 19 | ], 20 | framework: { 21 | name: getAbsolutePath('@storybook/react-vite'), 22 | options: {}, 23 | }, 24 | }; 25 | export default config; 26 | -------------------------------------------------------------------------------- /client/.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import type { Preview } from '@storybook/react'; 2 | import '@/index.css'; 3 | const preview: Preview = { 4 | parameters: { 5 | controls: { 6 | matchers: { 7 | color: /(background|color)$/i, 8 | date: /Date$/i, 9 | }, 10 | }, 11 | }, 12 | }; 13 | 14 | export default preview; 15 | -------------------------------------------------------------------------------- /client/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22-alpine 2 | 3 | WORKDIR /app 4 | 5 | COPY package.json yarn.lock .yarnrc.yml ./ 6 | COPY .yarn .yarn 7 | COPY client/ client/ 8 | 9 | RUN corepack enable && \ 10 | yarn install 11 | 12 | WORKDIR /app/client 13 | 14 | EXPOSE 5173 15 | CMD ["yarn", "run", "dev", "--no-open", "--host"] 16 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 13 | 14 | - Configure the top-level `parserOptions` property like this: 15 | 16 | ```js 17 | export default tseslint.config({ 18 | languageOptions: { 19 | // other options... 20 | parserOptions: { 21 | project: ['./tsconfig.node.json', './tsconfig.app.json'], 22 | tsconfigRootDir: import.meta.dirname, 23 | }, 24 | }, 25 | }) 26 | ``` 27 | 28 | - Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked` 29 | - Optionally add `...tseslint.configs.stylisticTypeChecked` 30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config: 31 | 32 | ```js 33 | // eslint.config.js 34 | import react from 'eslint-plugin-react' 35 | 36 | export default tseslint.config({ 37 | // Set the react version 38 | settings: { react: { version: '18.3' } }, 39 | plugins: { 40 | // Add the react plugin 41 | react, 42 | }, 43 | rules: { 44 | // other rules... 45 | // Enable its recommended rules 46 | ...react.configs.recommended.rules, 47 | ...react.configs['jsx-runtime'].rules, 48 | }, 49 | }) 50 | ``` 51 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | inear 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.0.1", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "preview": "vite preview", 9 | "storybook": "storybook dev -p 6006", 10 | "build-storybook": "storybook build" 11 | }, 12 | "dependencies": { 13 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0", 15 | "socket.io-client": "^4.8.1" 16 | }, 17 | "devDependencies": { 18 | "@chromatic-com/storybook": "^3.2.2", 19 | "@storybook/addon-essentials": "^8.4.2", 20 | "@storybook/addon-interactions": "^8.4.2", 21 | "@storybook/addon-onboarding": "^8.4.2", 22 | "@storybook/blocks": "^8.4.2", 23 | "@storybook/react": "^8.4.2", 24 | "@storybook/react-vite": "^8.4.2", 25 | "@storybook/test": "^8.4.2", 26 | "@tanstack/eslint-plugin-query": "^5.61.4", 27 | "@tanstack/react-query": "^5.61.5", 28 | "@tanstack/react-query-devtools": "^5.62.0", 29 | "@types/react": "^18.2.0", 30 | "@types/react-dom": "^18.2.0", 31 | "@vitejs/plugin-react": "^4.3.3", 32 | "autoprefixer": "^10.4.20", 33 | "axios": "^1.7.7", 34 | "fast-average-color": "^9.4.0", 35 | "hls.js": "^1.5.17", 36 | "lottie-react": "^2.4.0", 37 | "msw": "^2.6.1", 38 | "postcss": "^8.4.47", 39 | "react-error-boundary": "^4.1.2", 40 | "react-router-dom": "^6.28.0", 41 | "storybook": "^8.4.2", 42 | "swiper": "^11.1.15", 43 | "tailwindcss": "^3.4.14", 44 | "typescript": "^5.0.0", 45 | "vite": "^5.0.0", 46 | "vite-tsconfig-paths": "^5.1.0", 47 | "zustand": "^5.0.1" 48 | }, 49 | "packageManager": "yarn@4.5.1", 50 | "msw": { 51 | "workerDirectory": [ 52 | "public" 53 | ] 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /client/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/web18-inear/54ca8af29187610ecec421ad61b53789006f7b29/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet, Route, Routes } from 'react-router-dom'; 2 | import { MainPage } from '@/pages/MainPage'; 3 | import { StreamingPage } from '@/pages/StreamingPage'; 4 | import { AdminPage } from '@/pages/AdminPage'; 5 | import { AlbumPage } from '@/pages/AlbumPage'; 6 | import { AdminLoginPage } from '@/pages/AdminLoginPage'; 7 | import { ProtectedRoute } from '@/app/router/ProtectedRoute'; 8 | import { Sidebar } from './widgets/sidebar/ui/Sidebar'; 9 | import { GlobalBoundary } from './GlobalBoundary'; 10 | 11 | const MainLayout = () => ( 12 |
13 | 14 |
15 | 16 |
17 |
18 | ); 19 | 20 | export function App() { 21 | return ( 22 | 23 | 24 | }> 25 | } /> 26 | } /> 27 | } /> 28 | 29 | } /> 30 | 34 | 35 | 36 | } 37 | /> 38 | 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /client/src/GlobalBoundary.tsx: -------------------------------------------------------------------------------- 1 | import { ErrorBoundary, FallbackProps } from 'react-error-boundary'; 2 | import { Suspense } from 'react'; 3 | import { useNavigate } from 'react-router-dom'; 4 | import { Button } from './shared/ui'; 5 | 6 | export const GlobalErrorFallback = ({ 7 | error, 8 | resetErrorBoundary, 9 | }: FallbackProps) => { 10 | const navigate = useNavigate(); 11 | const navigateToMain = () => { 12 | navigate('/'); 13 | resetErrorBoundary(); 14 | }; 15 | return ( 16 |
17 |

에러가 발생했습니다.

18 |
{error.message}
19 | 20 |
21 | ); 22 | }; 23 | 24 | export const GlobalBoundary = ({ children }: { children: React.ReactNode }) => { 25 | return ( 26 | 27 | 로딩중...}>{children} 28 | 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /client/src/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from 'react-router-dom'; 2 | import { Sidebar } from './widgets/sidebar/ui/Sidebar'; 3 | 4 | export function Layout() { 5 | return ( 6 |
7 | 8 |
9 | 10 |
11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /client/src/NetworkBoundary.tsx: -------------------------------------------------------------------------------- 1 | import { ErrorBoundary, FallbackProps } from 'react-error-boundary'; 2 | import { Suspense } from 'react'; 3 | import { QueryErrorResetBoundary } from '@tanstack/react-query'; 4 | import { Button } from './shared/ui'; 5 | 6 | export const NetworkFallback = ({ 7 | error, 8 | resetErrorBoundary, 9 | }: FallbackProps) => { 10 | const handleClickReset = () => { 11 | resetErrorBoundary(); 12 | }; 13 | return ( 14 |
15 |

이 에러엔 슬픈 전설이 있어

16 |
{error.message}
17 | {/*
19 | ); 20 | }; 21 | 22 | export const NetworkBoundary = ({ 23 | children, 24 | }: { 25 | children: React.ReactNode; 26 | }) => { 27 | return ( 28 | 29 | {({ reset }) => ( 30 | 31 | 로딩중...} 33 | > 34 | {children} 35 | 36 | 37 | )} 38 | 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /client/src/app/router/ProtectedRoute.tsx: -------------------------------------------------------------------------------- 1 | import { Navigate, useLocation } from 'react-router-dom'; 2 | import { useEffect, useState } from 'react'; 3 | import axios from 'axios'; 4 | 5 | export function ProtectedRoute({ children }: { children: React.ReactNode }) { 6 | const [isAuthenticated, setIsAuthenticated] = useState(null); 7 | const location = useLocation(); 8 | 9 | useEffect(() => { 10 | const checkAuth = async () => { 11 | try { 12 | await axios.get( 13 | `${import.meta.env.VITE_API_URL}/api/admin/verify-token`, 14 | { 15 | withCredentials: true, 16 | }, 17 | ); 18 | setIsAuthenticated(true); 19 | } catch (error) { 20 | setIsAuthenticated(false); 21 | } 22 | }; 23 | 24 | checkAuth(); 25 | }, []); 26 | 27 | if (isAuthenticated === null) { 28 | return
Loading...
; 29 | } 30 | 31 | if (!isAuthenticated) { 32 | return ; 33 | } 34 | 35 | return <>{children}; 36 | } 37 | -------------------------------------------------------------------------------- /client/src/app/router/Router.tsx: -------------------------------------------------------------------------------- 1 | import { createBrowserRouter, RouterProvider } from 'react-router-dom'; 2 | import { routes } from './routes'; 3 | import { routerConfig } from './config'; 4 | 5 | const router = createBrowserRouter(routes, routerConfig); 6 | 7 | export function Router() { 8 | return ( 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /client/src/app/router/config.ts: -------------------------------------------------------------------------------- 1 | export const routerConfig = { 2 | future: { 3 | v7_fetcherPersist: true, 4 | v7_normalizeFormMethod: true, 5 | v7_partialHydration: true, 6 | v7_relativeSplatPath: true, 7 | v7_skipActionErrorRevalidation: true, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /client/src/app/router/index.ts: -------------------------------------------------------------------------------- 1 | export { Router } from './Router'; 2 | -------------------------------------------------------------------------------- /client/src/app/router/routes.tsx: -------------------------------------------------------------------------------- 1 | import { MainPage } from '@/pages/MainPage'; 2 | import { StreamingPage } from '@/pages/StreamingPage'; 3 | import { Layout } from '@/Layout'; 4 | import { AdminPage } from '@/pages/AdminPage'; 5 | import { AdminLoginPage } from '@/pages/AdminLoginPage'; 6 | import { ProtectedRoute } from '@/app/router/ProtectedRoute'; 7 | import { AlbumPage } from '@/pages/AlbumPage'; 8 | 9 | export const routes = [ 10 | { 11 | path: '/', 12 | element: , 13 | children: [ 14 | { 15 | path: '/', 16 | element: , 17 | }, 18 | { 19 | path: '/streaming/:roomId', 20 | element: , 21 | }, 22 | { 23 | path: '/album/:albumId', 24 | element: , 25 | }, 26 | ], 27 | }, 28 | { 29 | path: '/admin/login', 30 | element: , 31 | }, 32 | { 33 | path: '/admin', 34 | element: ( 35 | 36 | 37 | 38 | ), 39 | }, 40 | ]; 41 | -------------------------------------------------------------------------------- /client/src/assets/default-banner.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/web18-inear/54ca8af29187610ecec421ad61b53789006f7b29/client/src/assets/default-banner.webp -------------------------------------------------------------------------------- /client/src/assets/github-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/web18-inear/54ca8af29187610ecec421ad61b53789006f7b29/client/src/assets/github-logo.png -------------------------------------------------------------------------------- /client/src/assets/logo-album-cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/web18-inear/54ca8af29187610ecec421ad61b53789006f7b29/client/src/assets/logo-album-cover.png -------------------------------------------------------------------------------- /client/src/assets/logo-album-cover.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/web18-inear/54ca8af29187610ecec421ad61b53789006f7b29/client/src/assets/logo-album-cover.webp -------------------------------------------------------------------------------- /client/src/entities/album/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 스트리밍 페이지에서 사용되는 앨범 정보 3 | */ 4 | 5 | export interface RoomResponse { 6 | success: boolean; 7 | albumResponse: AlbumData; 8 | songResponseList: SongData[]; 9 | totalDuration: number; 10 | trackOrder: string; 11 | } 12 | 13 | export interface AlbumData { 14 | id: string; 15 | title: string; 16 | artist: string; 17 | tags: string; 18 | bannerUrl: string | null; 19 | jacketUrl: string | null; 20 | releaseDate: string; 21 | } 22 | 23 | export interface SongData { 24 | id: number; 25 | albumId: string; 26 | title: string; 27 | trackNumber: number; 28 | lyrics: string; 29 | composer: string; 30 | writer: string; 31 | producer: string; 32 | instrument: string; 33 | source: string; 34 | duration: number; 35 | } 36 | -------------------------------------------------------------------------------- /client/src/entities/albumRegister/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 관리자 페이지에서 앨범 등록을 위한 요청을 보낼 때 사용하는 타입 3 | */ 4 | export interface CreateAlbumRequest { 5 | title: string; 6 | artist: string; 7 | albumTag: string; 8 | releaseDate: string; 9 | songs?: Song[]; 10 | } 11 | 12 | export interface AlbumImageRequest { 13 | albumCover: any; 14 | bannerCover?: any; 15 | } 16 | 17 | /** 18 | * 앨범에 포함된 곡 정보 19 | */ 20 | export interface Song { 21 | title: string; 22 | trackNumber: string; 23 | lyrics?: string; 24 | composer: string; 25 | writer: string; 26 | instrument: string; 27 | producer: string; 28 | source?: string; 29 | } 30 | 31 | export interface SongFilesRequest { 32 | files: any[]; 33 | } 34 | -------------------------------------------------------------------------------- /client/src/entities/comment/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 앨범 상세 페이지 댓글 3 | */ 4 | export interface CommentData { 5 | albumId: string; 6 | content: string; 7 | createdAt: string; 8 | } 9 | 10 | /** 11 | * 앨범 상세 페이지 서버 데이터 12 | */ 13 | export interface AlbumDetailResponse { 14 | albumDetails: AlbumDetailData; 15 | songDetails: SongDetailData[]; 16 | } 17 | 18 | /** 19 | * 앨범 상세 앨범 정보 20 | */ 21 | export interface AlbumDetailData { 22 | albumId: string; 23 | albumName: string; 24 | artist: string; 25 | jacketUrl: string; 26 | } 27 | 28 | /** 29 | * 앨범 상세 페이지 노래 정보 30 | */ 31 | export interface SongDetailData { 32 | songName: string; 33 | songDuration: string; 34 | } 35 | -------------------------------------------------------------------------------- /client/src/entities/message/types.ts: -------------------------------------------------------------------------------- 1 | export interface MessageData { 2 | userName: string; 3 | message: string; 4 | userId: string; 5 | } 6 | -------------------------------------------------------------------------------- /client/src/entities/room/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 메인 페이지 사이드바 앨범 정보 3 | * 앨범의 간단한 정보만을 담고 있음 4 | */ 5 | export interface SidebarListResponse { 6 | streamingAlbums: AlbumData[]; 7 | upComingAlbums: AlbumData[]; 8 | } 9 | export interface AlbumData { 10 | albumId: number; 11 | albumName: string; 12 | albumTags?: string; 13 | } 14 | 15 | /** 16 | * 메인 페이지 배너 정보 17 | */ 18 | export interface bannerData { 19 | albumId: string; 20 | albumName: string; 21 | albumTags?: string; 22 | artist: string; 23 | bannerImageUrl: string; 24 | currentUserCount: number; 25 | releaseDate: string; 26 | } 27 | 28 | /** 29 | * 메인 페이지 최근 스트리밍 앨범 정보 30 | */ 31 | 32 | export interface EndedAlbumListResponse { 33 | endedAlbums: EndedAlbumData[]; 34 | } 35 | 36 | export interface EndedAlbumData { 37 | albumId: string; 38 | albumName: string; 39 | artist: string; 40 | albumTags?: string; 41 | jacketUrl?: string; 42 | } 43 | -------------------------------------------------------------------------------- /client/src/entities/songDetail/index.ts: -------------------------------------------------------------------------------- 1 | export { LyricsPanel } from './ui/LyricsPanel'; 2 | export { PlaylistPanel } from './ui/PlaylistPanel'; 3 | -------------------------------------------------------------------------------- /client/src/entities/songDetail/ui/LyricsPanel.css: -------------------------------------------------------------------------------- 1 | .lyrics::-webkit-scrollbar { 2 | background-color: #1e1e1e; 3 | width: 6px; 4 | } 5 | 6 | .lyrics::-webkit-scrollbar-thumb { 7 | background-color: #2a2a2a; 8 | /* border-radius: 4px; */ 9 | } 10 | -------------------------------------------------------------------------------- /client/src/entities/songDetail/ui/LyricsPanel.tsx: -------------------------------------------------------------------------------- 1 | import './LyricsPanel.css'; 2 | 3 | interface LyricsPanelProps { 4 | lyrics: string; 5 | } 6 | 7 | export function LyricsPanel({ lyrics }: LyricsPanelProps) { 8 | const lyricsFormatted = lyrics 9 | .split('\n') 10 | .map((line, index) =>

{line}

); 11 | return ( 12 |
13 | {lyricsFormatted} 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /client/src/entities/songDetail/ui/PlaylistPanel.css: -------------------------------------------------------------------------------- 1 | .playlist-scrollbar::-webkit-scrollbar { 2 | width: 0px; 3 | } 4 | 5 | .credit-scrollbar::-webkit-scrollbar { 6 | width: 6px; 7 | } 8 | 9 | .credit-scrollbar::-webkit-scrollbar-thumb { 10 | background-color: #2a2a2a; 11 | } 12 | -------------------------------------------------------------------------------- /client/src/entities/songDetail/ui/PlaylistPanel.tsx: -------------------------------------------------------------------------------- 1 | import { SongData } from '@/entities/album/types'; 2 | import { PlaylistPanelSong } from './PlaylistPanelSong'; 3 | import { PlaylistPanelCredit } from './PlaylistPanelCredit'; 4 | interface PlaylistPanelProps { 5 | songs: SongData[]; 6 | songIndex: number; 7 | } 8 | 9 | export function PlaylistPanel({ songs, songIndex }: PlaylistPanelProps) { 10 | return ( 11 |
12 | 13 | 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /client/src/entities/songDetail/ui/PlaylistPanelCredit.tsx: -------------------------------------------------------------------------------- 1 | import { SongData } from '@/entities/album/types'; 2 | import './PlaylistPanel.css'; 3 | 4 | interface PlaylistPanelCreditProps { 5 | currentSong: SongData; 6 | } 7 | 8 | function CreditListItem({ 9 | title, 10 | content, 11 | }: { 12 | title: string; 13 | content: string; 14 | }) { 15 | return ( 16 | <> 17 |

{title}

18 |

{content}

19 | 20 | ); 21 | } 22 | 23 | export function PlaylistPanelCredit({ currentSong }: PlaylistPanelCreditProps) { 24 | return ( 25 |
26 |

크레딧

27 | 28 | 32 | 33 | 34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /client/src/entities/songDetail/ui/PlaylistPanelSong.tsx: -------------------------------------------------------------------------------- 1 | import Lottie from 'lottie-react'; 2 | import musicAnimation from '@/shared/lottie/music.json'; 3 | import { SongData } from '@/entities/album/types'; 4 | import './PlaylistPanel.css'; 5 | interface PlaylistPanelSongProps { 6 | songs: SongData[]; 7 | songIndex: number; 8 | } 9 | 10 | export function PlaylistPanelSong({ 11 | songs, 12 | songIndex, 13 | }: PlaylistPanelSongProps) { 14 | const formattedDuration = (duration: number) => { 15 | const minutes = Math.floor(duration / 60); 16 | const seconds = duration % 60; 17 | return `${minutes}:${seconds < 10 ? `0${seconds}` : seconds}`; 18 | }; 19 | 20 | return ( 21 |
22 | {songs.map((song, index) => ( 23 |
28 | {songIndex - 1 === index ? ( 29 | 33 | ) : ( 34 |

{index + 1}

35 | )} 36 |

37 | {song.title} 38 |

39 |

40 | {formattedDuration(Number(song.duration))} 41 |

42 |
43 | ))} 44 |
45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /client/src/features/albumRegister/index.ts: -------------------------------------------------------------------------------- 1 | export { AlbumForm } from './ui/AlbumForm'; 2 | export { SongForm } from './ui/SongForm'; 3 | -------------------------------------------------------------------------------- /client/src/features/albumRegister/ui/AlbumForm.tsx: -------------------------------------------------------------------------------- 1 | import { Input } from '@/shared/ui'; 2 | interface AlbumFormProps { 3 | albumFormRef: React.RefObject; 4 | } 5 | export function AlbumForm({ albumFormRef }: AlbumFormProps) { 6 | return ( 7 |
8 | 9 | 10 | 15 | 16 |
17 | 18 | 19 |
20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /client/src/features/albumRegister/ui/SongForm.tsx: -------------------------------------------------------------------------------- 1 | import { Input } from '@/shared/ui'; 2 | 3 | interface SongFormProps { 4 | songFormRef: React.RefObject; 5 | } 6 | 7 | export function SongForm({ songFormRef }: SongFormProps) { 8 | return ( 9 |
10 |
11 | 12 |
13 |
14 | 15 | 16 | 21 |
22 |
23 | 24 | 25 | 26 |
27 |
28 | 29 |