๐ต ์ธ๋ ๋ฎค์ง์ EP ํธ๋๋ฆฌ์คํธ ํ์ด์ง - ์์ ์์ฒญ & ์คํธ๋ฆฌ๋ฐ ๋งํฌ ๋ชจ์
indie-ep-tracklist-page
๋ฌธ์ ์ค๋ช
ํ๋ก์ ํธ ๊ฐ์
์ธ๋ ๋ฎค์ง์ ์ด๋ ์๊ท๋ชจ ๋ ์ด๋ธ์ด ์ ๊ท EP(๋ฏธ๋์จ๋ฒ)๋ฅผ ํ๋ณดํ ์ ์๋ ํธ๋๋ฆฌ์คํธ ์๊ฐ ํ์ด์ง๋ฅผ ๋ง๋ญ๋๋ค. ๊ฐ ํธ๋์ ์ ๋ชฉ, ๋ฌ๋ํ์, 30์ด ๋ฏธ๋ฆฌ๋ฃ๊ธฐ ์ค๋์ค ํ๋ ์ด์ด, ๊ทธ๋ฆฌ๊ณ Spotify/Apple Music/YouTube ๋ฑ ์คํธ๋ฆฌ๋ฐ ํ๋ซํผ ๋งํฌ๋ฅผ ํ๋์ ๋ณด์ฌ์ฃผ๋ ํ์ด์ง์ ๋๋ค.
ํ์ ๊ธฐ๋ฅ
- ํธ๋ ๋ชฉ๋ก ์กฐํ: EP์ ์๋ก๋ ๊ณก ๋ชฉ๋ก์ ์นด๋ ํํ๋ก ํ์ (ํธ๋ ๋ฒํธ, ๊ณก ์ ๋ชฉ, ๋ฌ๋ํ์)
- ๋ฏธ๋ฆฌ๋ฃ๊ธฐ ํ๋ ์ด์ด: ๊ฐ ํธ๋๋ง๋ค HTML5 audio ํ๊ทธ๋ก 30์ด ์ํ ์ฌ์ (์ฌ์/์ผ์์ ์ง ๋ฒํผ)
- ์คํธ๋ฆฌ๋ฐ ๋งํฌ: Spotify, Apple Music, YouTube Music ๋ฑ ์ธ๋ถ ํ๋ซํผ ๋งํฌ ๋ฒํผ (์ ์ฐฝ ์ด๊ธฐ)
- ์จ๋ฒ ์ ๋ณด ํ์: EP ์ปค๋ฒ ์ด๋ฏธ์ง, ์จ๋ฒ๋ช , ์ํฐ์คํธ๋ช , ๋ฐ๋งค์ผ ์ ๋ณด๋ฅผ ์๋จ์ ๋ ธ์ถ
- ๋ฐ์ํ ๋์์ธ: ๋ชจ๋ฐ์ผ/ํ๋ธ๋ฆฟ/๋ฐ์คํฌํ ๋ชจ๋ ์ต์ ํ๋ ๋ ์ด์์ (Tailwind CSS ํ์ฉ)
์ ํ ์ถ๊ฐ ๊ธฐ๋ฅ (๋ณด๋์ค)
- ํธ๋ ์ข์์ ๊ธฐ๋ฅ (localStorage ๋๋ DB ์ ์ฅ)
- ์จ๋ฒ ์ ์ฒด ์ฌ์ ๋ฒํผ (์ฒซ ํธ๋๋ถํฐ ์์ฐจ ์ฌ์)
- ๊ฐ์ฌ ๋ชจ๋ฌ ํ์ (ํธ๋ ํด๋ฆญ ์ ๊ฐ์ฌ ๋ณด๊ธฐ)
- ๋คํฌ๋ชจ๋ ํ ๊ธ
- SNS ๊ณต์ ๋ฒํผ (Twitter, Facebook)
๊ธฐ์ ์คํ
- ๋ฐฑ์๋: Laravel 13.x (RESTful API)
- ํ๋ก ํธ์๋: React 19 (Vite)
- ๋ฐ์ดํฐ๋ฒ ์ด์ค: SQLite
- ์คํ์ผ๋ง: Tailwind CSS
- ์ค๋์ค: HTML5 Audio API
ํ์ต ๋ชฉํ
- Laravel API ๋ผ์ฐํธ์ ์ปจํธ๋กค๋ฌ๋ก JSON ์๋ต ๊ตฌํ
- React์์ fetch๋ก API ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ
- useState๋ก ์ค๋์ค ์ฌ์ ์ํ ๊ด๋ฆฌ
- Tailwind CSS๋ก ๋ฐ์ํ ์นด๋ ๋ ์ด์์ ๊ตฌ์ฑ
- ์ธ๋ถ ๋งํฌ ์ฐ๋ ๋ฐ ์ ์ฐฝ ์ด๊ธฐ ์ฒ๋ฆฌ
์ ์ฝ ์กฐ๊ฑด
- โข Laravel 13.x + React 19 + Vite ์กฐํฉ ์ฌ์ฉ (๋ณ๋ ํ๋ก์ ํธ ๊ตฌ์ฑ)
- โข ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ SQLite ์ฌ์ฉ
- โข ํ๋ก ํธ์๋๋ Vite๋ก React ํ๋ก์ ํธ ์์ฑ ํ fetch๋ก Laravel API ํธ์ถ
- โข ์ค๋์ค ํ์ผ์ public/storage ๋๋ ์ธ๋ถ CDN URL ์ฌ์ฉ
- โข Tailwind CSS๋ก ์คํ์ผ๋ง (CDN ๋๋ npm ์ค์น)
- โข ๋ณต์กํ ์ํ๊ด๋ฆฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ ๊ธ์ง (useState๋ง ์ฌ์ฉ)
- โข ์ค์๊ฐ ์ฑํ /WebSocket ๊ธฐ๋ฅ ๊ธ์ง
ํ๋กฌํํธ ๋ ์ํผ
์๋ ํ๋กฌํํธ๋ฅผ ์์๋๋ก AI์๊ฒ ๋ณด๋ด๋ฉด ๋ฉ๋๋ค. ๋ณต์ฌ ๋ฒํผ์ ๋๋ฌ ๋ฐ๋ก ์ฌ์ฉํ์ธ์.
mkdir indie-ep-tracklist-page && cd indie-ep-tracklist-page
์ด ํด๋์์ ํ๋ก์ ํธ๋ฅผ ์์ํฉ๋๋ค.
Laravel 13.x ๋ฐฑ์๋ API ํ๋ก์ ํธ๋ฅผ ์์ฑํ๊ณ ์ด๊ธฐ ์ค์ ์ ํด์ฃผ์ธ์.
1. Laravel 13.x ํ๋ก์ ํธ ์์ฑ:
- `composer create-project laravel/laravel backend`
- backend ํด๋๋ก ์ด๋: `cd backend`
2. ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค์ (.env):
- DB_CONNECTION=sqlite
- DB_DATABASE ๊ฒฝ๋ก๋ฅผ database/database.sqlite๋ก ์ค์
- `touch database/database.sqlite` ๋ช
๋ น์ผ๋ก ๋น SQLite ํ์ผ ์์ฑ
3. tracks ํ
์ด๋ธ ๋ง์ด๊ทธ๋ ์ด์
์์ฑ ๋ฐ ์คํ:
- php artisan make:migration create_tracks_table
- ๋ง์ด๊ทธ๋ ์ด์
ํ์ผ์ ๋ค์ ์ปฌ๋ผ ์ถ๊ฐ:
* id (primary key)
* track_number (integer, ํธ๋ ๋ฒํธ)
* title (string, ๊ณก ์ ๋ชฉ)
* duration (string, ๋ฌ๋ํ์ ์: '3:45')
* audio_url (string, nullable, 30์ด ์ํ ์ค๋์ค URL)
* spotify_url (string, nullable)
* apple_music_url (string, nullable)
* youtube_url (string, nullable)
* timestamps
- php artisan migrate ์คํ
4. Track ๋ชจ๋ธ ์์ฑ:
- php artisan make:model Track
- fillable ์์ฑ์ ์ ์ปฌ๋ผ๋ค ์ถ๊ฐ
5. API ๋ผ์ฐํธ ์ค์ (routes/api.php):
- GET /api/tracks (์ ์ฒด ํธ๋ ๋ชฉ๋ก ์กฐํ)
- GET /api/album-info (์จ๋ฒ ์ ๋ณด ์กฐํ - ํ๋์ฝ๋ฉ๋ JSON ๋ฐํ)
6. TracksController ์์ฑ ๋ฐ ๊ตฌํ:
- php artisan make:controller Api/TracksController
- index() ๋ฉ์๋: Track::orderBy('track_number')->get() ๋ฐํ
- albumInfo() ๋ฉ์๋: ์จ๋ฒ๋ช
, ์ํฐ์คํธ๋ช
, ๋ฐ๋งค์ผ, ์ปค๋ฒ ์ด๋ฏธ์ง URL์ JSON์ผ๋ก ๋ฐํ (์์ ๋ฐ์ดํฐ)
7. ์๋๋ก ์ํ ๋ฐ์ดํฐ 3๊ฐ ์์ฑ:
- php artisan make:seeder TracksSeeder
- 3๊ฐ ํธ๋ ์ํ (track_number: 1~3, title, duration, ์ค๋์ค URL์ placeholder)
- DatabaseSeeder์์ ํธ์ถ
- php artisan db:seed
8. CORS ์ค์ :
- config/cors.php์์ 'paths' => ['api/*'] ํ์ธ
- 'allowed_origins' => ['http://localhost:5173'] ์ถ๊ฐ (Vite ๊ธฐ๋ณธ ํฌํธ)
9. ์๋ฒ ์คํ:
- php artisan serve (http://localhost:8000)
์๋ฃ ํ Postman์ด๋ ๋ธ๋ผ์ฐ์ ์์ http://localhost:8000/api/tracks ์ ์ ์ JSON ๋ฐฐ์ด์ด ๋ฐํ๋๋์ง ํ์ธํด์ฃผ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: Laravel ๋ฐฑ์๋ API ์๋ฒ๊ฐ http://localhost:8000์์ ์คํ๋๋ฉฐ, /api/tracks ์๋ํฌ์ธํธ์์ 3๊ฐ์ ์ํ ํธ๋ ๋ฐ์ดํฐ๋ฅผ JSON์ผ๋ก ๋ฐํํฉ๋๋ค. /api/album-info ์๋ํฌ์ธํธ์์ ์จ๋ฒ ์ ๋ณด JSON์ ๋ฐํํฉ๋๋ค. SQLite ๋ฐ์ดํฐ๋ฒ ์ด์ค์ tracks ํ ์ด๋ธ์ด ์์ฑ๋๊ณ ์๋ ๋ฐ์ดํฐ๊ฐ ์ฝ์ ๋์ด ์์ต๋๋ค.
indie-ep-tracklist-page ๋ฃจํธ ํด๋๋ก ๋์์์ (cd ..) React 19 ํ๋ก ํธ์๋ ํ๋ก์ ํธ๋ฅผ ์์ฑํ๊ณ ๊ธฐ๋ณธ ์ค์ ์ ํด์ฃผ์ธ์.
1. Vite๋ก React ํ๋ก์ ํธ ์์ฑ:
- `npm create vite@latest frontend -- --template react`
- frontend ํด๋๋ก ์ด๋: `cd frontend`
- npm install ์คํ
2. Tailwind CSS ์ค์น ๋ฐ ์ค์ :
- `npm install -D tailwindcss postcss autoprefixer`
- `npx tailwindcss init -p`
- tailwind.config.js์ content์ "./index.html", "./src/**/*.{js,jsx}" ์ถ๊ฐ
- src/index.css์ Tailwind ๋๋ ํฐ๋ธ ์ถ๊ฐ:
```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```
3. ํ๋ก์ ํธ ๊ตฌ์กฐ ์ค์ :
- src/components ํด๋ ์์ฑ
- src/services ํด๋ ์์ฑ
4. API ์๋น์ค ํจ์ ์์ฑ (src/services/api.js):
- fetchTracks() ํจ์: fetch('http://localhost:8000/api/tracks')๋ก ํธ๋ ๋ชฉ๋ก ๊ฐ์ ธ์ค๊ธฐ
- fetchAlbumInfo() ํจ์: fetch('http://localhost:8000/api/album-info')๋ก ์จ๋ฒ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ
- ๋ ํจ์ ๋ชจ๋ async/await ์ฌ์ฉ, JSON ํ์ฑ ํ ๋ฐํ
5. ๊ฐ๋ฐ ์๋ฒ ์คํ:
- npm run dev (http://localhost:5173)
์๋ฃ ํ ๋ธ๋ผ์ฐ์ ์์ http://localhost:5173 ์ ์ ์ Vite + React ๊ธฐ๋ณธ ํ๋ฉด์ด ๋ณด์ด๋์ง ํ์ธํด์ฃผ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: React ํ๋ก ํธ์๋๊ฐ http://localhost:5173์์ ์คํ๋๋ฉฐ, Tailwind CSS๊ฐ ์ค์ ๋์ด ์์ต๋๋ค. src/services/api.js์ Laravel API๋ฅผ ํธ์ถํ๋ ํจ์๊ฐ ์์ฑ๋์ด ์์ต๋๋ค. ๋ฐฑ์๋(8000 ํฌํธ)์ ํ๋ก ํธ์๋(5173 ํฌํธ)๊ฐ ๋์์ ์คํ ๊ฐ๋ฅํ ์ํ์ ๋๋ค.
React ํ๋ก ํธ์๋์์ ์จ๋ฒ ์ ๋ณด์ ํธ๋๋ฆฌ์คํธ๋ฅผ ํ๋ฉด์ ํ์ํ๋ UI๋ฅผ ๊ตฌํํด์ฃผ์ธ์.
1. AlbumHeader ์ปดํฌ๋ํธ ์์ฑ (src/components/AlbumHeader.jsx):
- props๋ก albumInfo ๋ฐ๊ธฐ (coverImage, albumName, artistName, releaseDate)
- ์จ๋ฒ ์ปค๋ฒ ์ด๋ฏธ์ง (์ต๋ 300px), ์จ๋ฒ๋ช
, ์ํฐ์คํธ๋ช
, ๋ฐ๋งค์ผ์ Tailwind๋ก ์คํ์ผ๋ง
- ๋ฐ์ํ: ๋ชจ๋ฐ์ผ์์๋ ์ธ๋ก ๋ฐฐ์น, ๋ฐ์คํฌํ์์๋ ๊ฐ๋ก ๋ฐฐ์น (flex)
2. TrackCard ์ปดํฌ๋ํธ ์์ฑ (src/components/TrackCard.jsx):
- props๋ก track ๊ฐ์ฒด ๋ฐ๊ธฐ (track_number, title, duration, audio_url, spotify_url, apple_music_url, youtube_url)
- ์นด๋ ๋ ์ด์์:
* ์ผ์ชฝ: ํธ๋ ๋ฒํธ (ํฐ ํฐํธ, ํ์ ์ํ ๋ฐฐ๊ฒฝ)
* ์ค์: ๊ณก ์ ๋ชฉ + ๋ฌ๋ํ์
* ์ค๋ฅธ์ชฝ: ์ฌ์ ๋ฒํผ (์์ด์ฝ ๋๋ "โถ" ํ
์คํธ)
- ์คํธ๋ฆฌ๋ฐ ๋งํฌ ๋ฒํผ๋ค (Spotify, Apple Music, YouTube):
* ๊ฐ ๋งํฌ๊ฐ ์์ ๋๋ง ๋ฒํผ ํ์ (์กฐ๊ฑด๋ถ ๋ ๋๋ง)
* ๋ฒํผ ํด๋ฆญ ์ target="_blank" rel="noopener noreferrer"๋ก ์ ์ฐฝ ์ด๊ธฐ
* Tailwind๋ก ๋ฒํผ ์คํ์ผ๋ง (bg-green-500, bg-red-500 ๋ฑ)
- ์นด๋ ์ ์ฒด์ hover ํจ๊ณผ (shadow-lg, scale-105)
3. App.jsx ์์ :
- useState๋ก albumInfo, tracks ์ํ ๊ด๋ฆฌ
- useEffect๋ก ์ปดํฌ๋ํธ ๋ง์ดํธ ์ fetchAlbumInfo(), fetchTracks() ํธ์ถ
- ๋ก๋ฉ ์ํ ์ฒ๋ฆฌ (loading์ด true์ผ ๋ "Loading..." ํ์)
- ์๋ฌ ์ฒ๋ฆฌ (fetch ์คํจ ์ ์๋ฌ ๋ฉ์์ง ํ์)
- AlbumHeader ์ปดํฌ๋ํธ์ albumInfo ์ ๋ฌ
- tracks ๋ฐฐ์ด์ map์ผ๋ก ์ํํ๋ฉฐ TrackCard ์ปดํฌ๋ํธ ๋ ๋๋ง
4. ์ ์ฒด ๋ ์ด์์:
- ๋ฐฐ๊ฒฝ์: bg-gray-100 (์ฐํ ํ์)
- ์ต๋ ๋๋น ์ปจํ
์ด๋: max-w-4xl mx-auto py-8 px-4
- AlbumHeader์ ํธ๋๋ฆฌ์คํธ ์ฌ์ด ์ฌ๋ฐฑ: mt-8
์๋ฃ ํ ๋ธ๋ผ์ฐ์ ์์ ์จ๋ฒ ์ ๋ณด์ 3๊ฐ์ ํธ๋ ์นด๋๊ฐ ๋ณด์ด๋์ง, ์คํธ๋ฆฌ๋ฐ ๋งํฌ ๋ฒํผ ํด๋ฆญ ์ ์ ์ฐฝ์ด ์ด๋ฆฌ๋์ง ํ์ธํด์ฃผ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: React ํ๋ก ํธ์๋ ํ๋ฉด์ ์จ๋ฒ ์ปค๋ฒ, ์จ๋ฒ๋ช , ์ํฐ์คํธ๋ช , ๋ฐ๋งค์ผ์ด ์๋จ์ ํ์๋๊ณ , ๊ทธ ์๋ 3๊ฐ์ ํธ๋ ์นด๋๊ฐ ์ธ๋ก๋ก ๋์ด๋ฉ๋๋ค. ๊ฐ ์นด๋์๋ ํธ๋ ๋ฒํธ, ๊ณก ์ ๋ชฉ, ๋ฌ๋ํ์, ์คํธ๋ฆฌ๋ฐ ๋งํฌ ๋ฒํผ(Spotify, Apple Music, YouTube)์ด ํ์๋ฉ๋๋ค. ๋ฐ์ํ ๋์์ธ์ด ์ ์ฉ๋์ด ๋ชจ๋ฐ์ผ/๋ฐ์คํฌํ ๋ชจ๋ ์ ๋ณด์ ๋๋ค.
TrackCard ์ปดํฌ๋ํธ์ HTML5 audio ํ๊ทธ๋ฅผ ์ฌ์ฉํ ๋ฏธ๋ฆฌ๋ฃ๊ธฐ ํ๋ ์ด์ด๋ฅผ ์ถ๊ฐํด์ฃผ์ธ์.
1. TrackCard ์ปดํฌ๋ํธ ์์ :
- useState๋ก isPlaying ์ํ ์ถ๊ฐ (์ด๊ธฐ๊ฐ: false)
- useRef๋ก audioRef ์์ฑ (audio ํ๊ทธ ์ฐธ์กฐ์ฉ)
2. audio ํ๊ทธ ์ถ๊ฐ:
- src={track.audio_url}
- ref={audioRef}
- onEnded ์ด๋ฒคํธ: ์ฌ์ ์๋ฃ ์ isPlaying์ false๋ก ๋ณ๊ฒฝ
- ํ๊ทธ๋ ์จ๊น ์ฒ๋ฆฌ (className="hidden")
3. ์ฌ์/์ผ์์ ์ง ๋ฒํผ ๊ตฌํ:
- ๋ฒํผ ํด๋ฆญ ์ togglePlay ํจ์ ์คํ
- togglePlay ํจ์:
* isPlaying์ด false๋ฉด audioRef.current.play() ํธ์ถ, isPlaying์ true๋ก ๋ณ๊ฒฝ
* isPlaying์ด true๋ฉด audioRef.current.pause() ํธ์ถ, isPlaying์ false๋ก ๋ณ๊ฒฝ
- ๋ฒํผ ์์ด์ฝ:
* isPlaying์ด true์ผ ๋: "โธ" (์ผ์์ ์ง ์์ด์ฝ)
* isPlaying์ด false์ผ ๋: "โถ" (์ฌ์ ์์ด์ฝ)
- ๋ฒํผ ์คํ์ผ: bg-blue-500 hover:bg-blue-600 text-white rounded-full p-3
4. audio_url์ด ์๋ ๊ฒฝ์ฐ ์ฒ๋ฆฌ:
- track.audio_url์ด null์ด๋ฉด ์ฌ์ ๋ฒํผ ๋นํ์ฑํ (opacity-50 cursor-not-allowed)
- ๋ฒํผ ํด๋ฆญํด๋ ๋์ํ์ง ์๋๋ก ์กฐ๊ฑด ์ถ๊ฐ
5. ์๋ ๋ฐ์ดํฐ ์
๋ฐ์ดํธ (backend):
- backend/database/seeders/TracksSeeder.php ์์
- audio_url์ ๋ฌด๋ฃ ์ํ ์ค๋์ค URL ์ถ๊ฐ (์: https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3)
- ๋๋ Laravel public/audio ํด๋์ mp3 ํ์ผ ์ถ๊ฐ ํ http://localhost:8000/audio/sample1.mp3 ํ์์ผ๋ก URL ์ค์
- php artisan migrate:fresh --seed ์ฌ์คํ
์๋ฃ ํ ๋ธ๋ผ์ฐ์ ์์ ์ฌ์ ๋ฒํผ ํด๋ฆญ ์ ์ค๋์ค๊ฐ ์ฌ์๋๊ณ , ์์ด์ฝ์ด ์ผ์์ ์ง ๋ชจ์์ผ๋ก ๋ณ๊ฒฝ๋๋์ง ํ์ธํด์ฃผ์ธ์. ์ฌ์ ์๋ฃ ์ ์๋์ผ๋ก ๋ฒํผ์ด ์ฌ์ ์์ด์ฝ์ผ๋ก ๋์์ค๋์ง๋ ํ์ธํ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: ๊ฐ ํธ๋ ์นด๋์ ์ฌ์/์ผ์์ ์ง ๋ฒํผ์ด ์ถ๊ฐ๋๋ฉฐ, ํด๋ฆญ ์ HTML5 audio๋ก 30์ด ์ํ์ด ์ฌ์๋ฉ๋๋ค. ์ฌ์ ์ค์๋ ์ผ์์ ์ง ์์ด์ฝ(โธ)์ผ๋ก ๋ณ๊ฒฝ๋๊ณ , ์ผ์์ ์งํ๊ฑฐ๋ ์ฌ์์ด ๋๋๋ฉด ์ฌ์ ์์ด์ฝ(โถ)์ผ๋ก ๋์์ต๋๋ค. audio_url์ด ์๋ ํธ๋์ ๋ฒํผ์ด ๋นํ์ฑํ๋ฉ๋๋ค.
์ ์ถ๋ ์ํ (0)
๋ก๊ทธ์ธ ํ ์ ์ถ์์ง ์ ์ถ๋ ์ํ์ด ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ ๋์ ์๊ฐ ๋์ด๋ณด์ธ์!