๐ฌ ์ธ๋ ์ํ์ ์์์ ํฌํ ํ๋ซํผ - ๊ด๊ฐ์ด ์ ํํ๋ ์ต๊ณ ์ ์ํ
indie-film-festival-voting
๋ฌธ์ ์ค๋ช
ํ๋ก์ ํธ ๊ฐ์
์ธ๋ ์ํ์ ์์ ๊ด๊ฐ๋ค์ด ์์์์ ํฌํํ๊ณ ์ค์๊ฐ์ผ๋ก ์์๋ฅผ ํ์ธํ ์ ์๋ ํ์คํ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ๋๋ค. Laravel 13.x๋ก RESTful API๋ฅผ ๊ตฌ์ถํ๊ณ , React 19๋ก ์ธํฐ๋ํฐ๋ธํ ํ๋ก ํธ์๋๋ฅผ ๋ง๋ญ๋๋ค.
ํ์ ๊ธฐ๋ฅ
- ์ํ ๋ชฉ๋ก ์กฐํ: ์ํ์ ์ ์ถํ๋ ์ํ๋ค์ ์นด๋ ํ์์ผ๋ก ํ์ (ํฌ์คํฐ, ์ ๋ชฉ, ๊ฐ๋ , ์๋์์ค)
- ํฌํ ๊ธฐ๋ฅ: ๊ฐ ์ํ์ ์ข์์ ๋ฒํผ์ ํด๋ฆญํ์ฌ ํฌํ (ํ ์ํ๋น 1ํ ์ ํ)
- ์ค์๊ฐ ์์: ํฌํ์ ๊ธฐ์ค์ผ๋ก ์๋ ์ ๋ ฌ๋๋ ์ค์๊ฐ ์์ ํ์
- ํฌํ ํํฉ ์๊ฐํ: ๊ฐ ์ํ์ ์ด ํฌํ์๋ฅผ ๋ฐฐ์ง๋ก ํ์
- ๋ฐ์ํ ๋์์ธ: ๋ชจ๋ฐ์ผ/ํ๋ธ๋ฆฟ/๋ฐ์คํฌํฑ ๋ชจ๋ ์ง์
์ ํ ์ถ๊ฐ ๊ธฐ๋ฅ (๋ณด๋์ค)
- ์ํ ํํฐ๋ง (์ฅ๋ฅด๋ณ, ์์์ผ์๋ณ)
- ํฌํ ์ทจ์ ๊ธฐ๋ฅ
- ๊ด๊ฐ ์ฝ๋ฉํธ ๋จ๊ธฐ๊ธฐ (๊ฐ๋จํ ํ์คํ)
- ๋ด๊ฐ ํฌํํ ์ํ ํ์ด๋ผ์ดํธ ํ์
- ์ ๋๋ฉ์ด์ ํจ๊ณผ (ํฌํ ์ ํํธ ์ดํํธ)
๊ธฐ์ ์คํ
- Backend: Laravel 13.x (RESTful API)
- Frontend: React 19 (Vite)
- Database: SQLite
- Styling: Tailwind CSS
- HTTP Client: Axios
ํ์ต ํฌ์ธํธ
- Laravel API ๋ผ์ฐํธ ์ค๊ณ ๋ฐ ์ปจํธ๋กค๋ฌ ์์ฑ
- React์์ REST API ์ฐ๋ (fetch/axios)
- useState๋ฅผ ํ์ฉํ ์ํ ๊ด๋ฆฌ
- ์ค์๊ฐ ๋ฐ์ดํฐ ์ ๋ฐ์ดํธ (์ฌํฌํ ์ ์ฆ์ ๋ฐ์)
- ๋ฐ์ํ ๊ทธ๋ฆฌ๋ ๋ ์ด์์ ๊ตฌํ
์ ์ฝ ์กฐ๊ฑด
- โข Laravel 13.x ์ต์ ๋ฒ์ ์ฌ์ฉ ํ์
- โข React 19 ์ต์ ๋ฒ์ ์ฌ์ฉ ํ์
- โข SQLite ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฌ์ฉ (๊ฐํธํ ์ค์ )
- โข Laravel์ API ์๋ฒ๋ก๋ง ๋์ (๋ณ๋ Vite React ํ๋ก์ ํธ)
- โข Tailwind CSS๋ก ์คํ์ผ๋ง
- โข ์ค์๊ฐ ์ฑํ /WebSocket ๊ธฐ๋ฅ ๊ธ์ง (HTTP ๊ธฐ๋ฐ ํฌํ๋ง)
- โข ๋ณต์กํ ์ํ๊ด๋ฆฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ ๊ธ์ง (useState๋ง ์ฌ์ฉ)
- โข ๊ด๋ฆฌ์ ๊ธฐ๋ฅ ์์ (๊ด๊ฐ์ฉ ํฌํ ํ์ด์ง๋ง)
- โข ์ต์ 5๊ฐ ์ด์์ ์ํ ์ํ ๋ฐ์ดํฐ ์๋
- โข CORS ์ค์ ํฌํจ (Laravel API โ React ํต์ )
ํ๋กฌํํธ ๋ ์ํผ
์๋ ํ๋กฌํํธ๋ฅผ ์์๋๋ก AI์๊ฒ ๋ณด๋ด๋ฉด ๋ฉ๋๋ค. ๋ณต์ฌ ๋ฒํผ์ ๋๋ฌ ๋ฐ๋ก ์ฌ์ฉํ์ธ์.
mkdir indie-film-festival-voting && cd indie-film-festival-voting
์ด ํด๋์์ ํ๋ก์ ํธ๋ฅผ ์์ํฉ๋๋ค.
# Laravel 13.x API ๋ฐฑ์๋ ์ค์
1. Laravel 13.x ํ๋ก์ ํธ๋ฅผ ์์ฑํ์ธ์:
- ํ๋ก์ ํธ๋ช
: `backend`
- SQLite ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฌ์ฉ
- `.env` ํ์ผ์์ `DB_CONNECTION=sqlite` ์ค์
- `database/database.sqlite` ํ์ผ ์์ฑ
2. ์ํ(Film) ๋ชจ๋ธ๊ณผ ๋ง์ด๊ทธ๋ ์ด์
์ ์์ฑํ์ธ์:
- ํ
์ด๋ธ๋ช
: `films`
- ์ปฌ๋ผ:
- `id` (primary key)
- `title` (string, ์ํ ์ ๋ชฉ)
- `director` (string, ๊ฐ๋
์ด๋ฆ)
- `synopsis` (text, ์๋์์ค)
- `poster_url` (string, ํฌ์คํฐ ์ด๋ฏธ์ง URL)
- `genre` (string, ์ฅ๋ฅด)
- `votes` (integer, ํฌํ์, ๊ธฐ๋ณธ๊ฐ 0)
- `timestamps`
3. ๋ง์ด๊ทธ๋ ์ด์
์ ์คํํ์ธ์.
4. 5๊ฐ ์ด์์ ์ํ ์ํ ๋ฐ์ดํฐ๋ฅผ ์๋๋ก ์์ฑํ์ธ์:
- ๋ค์ํ ์ฅ๋ฅด (๋๋ผ๋ง, ์ค๋ฆด๋ฌ, ๋ก๋งจ์ค, SF, ๋คํ๋ฉํฐ๋ฆฌ ๋ฑ)
- ์ค์ ์ธ๋ ์ํ ๋๋์ด ๋๋ ์ ๋ชฉ๊ณผ ์๋์์ค
- ํฌ์คํฐ ์ด๋ฏธ์ง๋ placeholder ์ด๋ฏธ์ง URL ์ฌ์ฉ (์: https://via.placeholder.com/300x450)
5. ์๋๋ฅผ ์คํํ์ธ์.
๋ช
๋ น์ด์ ์ฝ๋๋ฅผ ๋ชจ๋ ์ ๊ณตํด์ฃผ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: Laravel 13.x ํ๋ก์ ํธ๊ฐ ์์ฑ๋๊ณ , Film ๋ชจ๋ธ๊ณผ ๋ง์ด๊ทธ๋ ์ด์ ์ด ์๋ฃ๋๋ฉฐ, 5๊ฐ ์ด์์ ์ํ ์ํ ๋ฐ์ดํฐ๊ฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅ๋ฉ๋๋ค. `php artisan migrate --seed` ์คํ ํ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ํ ๋ ์ฝ๋๊ฐ ์กด์ฌํด์ผ ํฉ๋๋ค.
# Laravel API ๋ผ์ฐํธ ๋ฐ ์ปจํธ๋กค๋ฌ ๊ตฌํ
1. FilmController๋ฅผ ์์ฑํ๊ณ ๋ค์ API ์๋ํฌ์ธํธ๋ฅผ ๊ตฌํํ์ธ์:
- `GET /api/films`: ๋ชจ๋ ์ํ ๋ชฉ๋ก ์กฐํ (votes ๋ด๋ฆผ์ฐจ์ ์ ๋ ฌ)
- `POST /api/films/{id}/vote`: ํน์ ์ํ์ ํฌํ (votes ์นด์ดํธ +1)
2. `routes/api.php`์ ๋ผ์ฐํธ๋ฅผ ๋ฑ๋กํ์ธ์.
3. CORS ์ค์ ์ ์ถ๊ฐํ์ธ์:
- `config/cors.php`์์ `http://localhost:5173` (React Vite ๊ธฐ๋ณธ ํฌํธ) ํ์ฉ
- ๋๋ `php artisan install:api` ๋ช
๋ น์ผ๋ก API ๊ธฐ๋ณธ ์ค์ ์ ์ฉ
4. FilmController์ `vote` ๋ฉ์๋ ๊ตฌํ ์:
- ์ํ ID๋ก ๋ ์ฝ๋๋ฅผ ์ฐพ๊ณ
- votes ์ปฌ๋ผ์ +1 ์ฆ๊ฐ
- ์
๋ฐ์ดํธ๋ ์ํ ๋ฐ์ดํฐ๋ฅผ JSON์ผ๋ก ๋ฐํ
5. Postman์ด๋ curl๋ก API ํ
์คํธ:
- `GET http://localhost:8000/api/films`
- `POST http://localhost:8000/api/films/1/vote`
๋ชจ๋ ์ฝ๋์ ํ
์คํธ ๊ฒฐ๊ณผ๋ฅผ ์ ๊ณตํด์ฃผ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: Laravel API ์๋ฒ๊ฐ ์ ์ ๋์ํ๋ฉฐ, GET /api/films๋ก ์ํ ๋ชฉ๋ก์ JSON ํ์์ผ๋ก ๋ฐ์ ์ ์๊ณ , POST /api/films/{id}/vote๋ก ํฌํ ์ votes ๊ฐ์ด ์ฆ๊ฐํฉ๋๋ค. CORS ์ค์ ์ด ์๋ฃ๋์ด React ์ฑ์์ API๋ฅผ ํธ์ถํ ์ ์์ต๋๋ค.
# React 19 ํ๋ก ํธ์๋ ํ๋ก์ ํธ ์์ฑ 1. `indie-film-festival-voting` ํด๋์์ React ํ๋ก์ ํธ๋ฅผ ์์ฑํ์ธ์: - Vite ์ฌ์ฉ - ํ๋ก์ ํธ๋ช : `frontend` - React 19 ์ต์ ๋ฒ์ ํ์ธ 2. Tailwind CSS๋ฅผ ์ค์นํ๊ณ ์ค์ ํ์ธ์: - `npm install -D tailwindcss postcss autoprefixer` - `npx tailwindcss init -p` - `tailwind.config.js`์์ content ๊ฒฝ๋ก ์ค์ - `src/index.css`์ Tailwind directives ์ถ๊ฐ 3. Axios๋ฅผ ์ค์นํ์ธ์: - `npm install axios` 4. ๊ธฐ๋ณธ ํ๋ก์ ํธ ๊ตฌ์กฐ: - `src/components/FilmCard.jsx` (์ํ ์นด๋ ์ปดํฌ๋ํธ) - `src/App.jsx` (๋ฉ์ธ ์ฑ ์ปดํฌ๋ํธ) 5. `npm run dev`๋ก ๊ฐ๋ฐ ์๋ฒ๋ฅผ ์คํํ๊ณ http://localhost:5173์์ ํ์ธํ์ธ์. ๋ชจ๋ ์ค์น ๋ช ๋ น์ด์ ์ด๊ธฐ ์ค์ ์ฝ๋๋ฅผ ์ ๊ณตํด์ฃผ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: React 19 ํ๋ก์ ํธ๊ฐ ์์ฑ๋๊ณ , Tailwind CSS์ Axios๊ฐ ์ค์น๋๋ฉฐ, ๊ฐ๋ฐ ์๋ฒ๊ฐ ์ ์์ ์ผ๋ก ์คํ๋ฉ๋๋ค. ๋ธ๋ผ์ฐ์ ์์ ๊ธฐ๋ณธ React ํ์ด์ง๊ฐ ํ์๋ฉ๋๋ค.
# React ์ฑ ๊ตฌํ: ์ํ ๋ชฉ๋ก ์กฐํ ๋ฐ ํ์
1. `src/App.jsx`๋ฅผ ์์ ํ์ธ์:
- useState๋ก films ์ํ ๊ด๋ฆฌ
- useEffect๋ก ์ปดํฌ๋ํธ ๋ง์ดํธ ์ Laravel API์์ ์ํ ๋ชฉ๋ก fetch
- axios.get('http://localhost:8000/api/films')๋ก ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ
- ๋ก๋ฉ ์ํ์ ์๋ฌ ์ฒ๋ฆฌ ํฌํจ
2. `src/components/FilmCard.jsx` ์ปดํฌ๋ํธ๋ฅผ ์์ฑํ์ธ์:
- props: film ๊ฐ์ฒด, onVote ํจ์
- ์ํ ํฌ์คํฐ, ์ ๋ชฉ, ๊ฐ๋
, ์๋์์ค, ์ฅ๋ฅด ํ์
- ํฌํ์ ๋ฐฐ์ง (์ฐ์ธก ์๋จ์ ํํธ ์์ด์ฝ๊ณผ ํจ๊ป)
- "ํฌํํ๊ธฐ" ๋ฒํผ (ํด๋ฆญ ์ onVote ํธ์ถ)
- Tailwind CSS๋ก ์นด๋ ์คํ์ผ๋ง (๊ทธ๋ฆผ์, ๋ผ์ด๋ ๋ชจ์๋ฆฌ, ํธ๋ฒ ํจ๊ณผ)
3. App.jsx์์ films ๋ฐฐ์ด์ map์ผ๋ก ์ํํ๋ฉฐ FilmCard ๋ ๋๋ง:
- ๊ทธ๋ฆฌ๋ ๋ ์ด์์ (๋ชจ๋ฐ์ผ 1์ด, ํ๋ธ๋ฆฟ 2์ด, ๋ฐ์คํฌํฑ 3์ด)
- gap๊ณผ padding ์ ์ฉ
4. ํค๋ ์น์
์ถ๊ฐ:
- ํ์ด์ง ์ ๋ชฉ: "๐ฌ ์ธ๋ ์ํ์ ๊ด๊ฐ ํฌํ"
- ๋ถ์ : "๋น์ ์ด ์ ํํ๋ ์ต๊ณ ์ ์ํ"
๋ชจ๋ React ์ปดํฌ๋ํธ ์ฝ๋๋ฅผ ์ ๊ณตํด์ฃผ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: React ์ฑ์์ Laravel API๋ก๋ถํฐ ์ํ ๋ชฉ๋ก์ ์ฑ๊ณต์ ์ผ๋ก ๊ฐ์ ธ์ ํ๋ฉด์ ์นด๋ ํ์์ผ๋ก ํ์๋ฉ๋๋ค. ๊ฐ ์นด๋์๋ ํฌ์คํฐ, ์ ๋ชฉ, ๊ฐ๋ , ์๋์์ค, ์ฅ๋ฅด, ํ์ฌ ํฌํ์, ํฌํ ๋ฒํผ์ด ํฌํจ๋์ด ์์ผ๋ฉฐ, ๋ฐ์ํ ๊ทธ๋ฆฌ๋ ๋ ์ด์์์ด ์ ์ฉ๋ฉ๋๋ค.
# ํฌํ ๊ธฐ๋ฅ ๊ตฌํ ๋ฐ ์ค์๊ฐ ์
๋ฐ์ดํธ
1. `src/App.jsx`์ handleVote ํจ์๋ฅผ ์ถ๊ฐํ์ธ์:
- ํ๋ผ๋ฏธํฐ: filmId
- axios.post(`http://localhost:8000/api/films/${filmId}/vote`)๋ก ํฌํ ์์ฒญ
- ์ฑ๊ณต ์ films ์ํ๋ฅผ ์
๋ฐ์ดํธ (ํด๋น ์ํ์ votes +1)
- ์๋ฌ ์ฒ๋ฆฌ ํฌํจ
2. handleVote ํจ์๋ฅผ FilmCard ์ปดํฌ๋ํธ์ onVote prop์ผ๋ก ์ ๋ฌํ์ธ์.
3. FilmCard์์ ํฌํ ๋ฒํผ ํด๋ฆญ ์:
- onVote(film.id) ํธ์ถ
- ๋ฒํผ์ ๋ก๋ฉ ์ํ ํ์ (ํด๋ฆญ ํ ์ ๊น ๋นํ์ฑํ)
- ํฌํ ํ ์ฑ๊ณต ํผ๋๋ฐฑ (๋ฒํผ ์์ ๋ณ๊ฒฝ ๋๋ ์ฒดํฌ ์์ด์ฝ)
4. ํฌํ ํ ์๋์ผ๋ก ์ํ ๋ชฉ๋ก ์ฌ์ ๋ ฌ:
- votes ๋ด๋ฆผ์ฐจ์์ผ๋ก ์ ๋ ฌ
- 1์ ์ํ์ "๐ 1์" ๋ฐฐ์ง ํ์
5. ์คํ์ผ๋ง ๊ฐ์ :
- ํฌํ ๋ฒํผ ํธ๋ฒ ์ ์ ๋๋ฉ์ด์
(scale-105)
- ํฌํ์ ๋ฐฐ์ง์ ๊ทธ๋ผ๋ฐ์ด์
๋ฐฐ๊ฒฝ
- ํฌ์คํฐ ์ด๋ฏธ์ง ๋ก๋ฉ ์ skeleton UI
๋ชจ๋ ์ฝ๋์ ์คํ์ผ๋ง์ ์ ๊ณตํด์ฃผ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: ํฌํ ๋ฒํผ์ ํด๋ฆญํ๋ฉด Laravel API์ POST ์์ฒญ์ด ์ ์ก๋๊ณ , ์ฑ๊ณต ์ ํด๋น ์ํ์ ํฌํ์๊ฐ ์ฆ์ ์ฆ๊ฐํ๋ฉฐ ํ๋ฉด์ ๋ฐ์๋ฉ๋๋ค. ์ํ ๋ชฉ๋ก์ด ํฌํ์ ๊ธฐ์ค์ผ๋ก ์๋ ์ฌ์ ๋ ฌ๋๊ณ , 1์ ์ํ์ ํธ๋กํผ ๋ฐฐ์ง๊ฐ ํ์๋ฉ๋๋ค. ๋ฒํผ ์ ๋๋ฉ์ด์ ๊ณผ ์๊ฐ์ ํผ๋๋ฐฑ์ด ์๋ํฉ๋๋ค.
์ ์ถ๋ ์ํ (0)
๋ก๊ทธ์ธ ํ ์ ์ถ์์ง ์ ์ถ๋ ์ํ์ด ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ ๋์ ์๊ฐ ๋์ด๋ณด์ธ์!