๐๏ธ ๊ณต์ฐ ํฐ์ผ ์๋ ๊ฒ์ํ - ๋ชป ๊ฐ๋ ๊ณต์ฐ ํฐ์ผ ๋๋ํ๊ธฐ
concert-ticket-giveaway-board
๋ฌธ์ ์ค๋ช
๐ฏ ํ๋ก์ ํธ ๊ฐ์
๊ฐ์๊ธฐ ์ผ์ ์ด ์๊ฒจ์ ๋ชป ๊ฐ๊ฒ ๋ ๊ณต์ฐ ํฐ์ผ์ ๋ค๋ฅธ ์ฌ๋์๊ฒ ์๋ํ๊ฑฐ๋, ์ํ๋ ๊ณต์ฐ์ ์๋ ํฐ์ผ์ ์ฐพ์๋ณผ ์ ์๋ ๊ฒ์ํ์ ๋ง๋ญ๋๋ค.
โจ ํ์ ๊ธฐ๋ฅ
๋ฐฑ์๋ (Laravel 13.x API)
- ํฐ์ผ ์๋ ๊ธ CRUD: ๊ณต์ฐ๋ช , ๋ ์ง, ์ฅ์, ๋งค์, ๊ฐ๊ฒฉ, ์ฐ๋ฝ์ฒ ๋ฑ ์ ๋ณด ์ ์ฅ
- RESTful API ์ค๊ณ:
/api/tickets์๋ํฌ์ธํธ๋ก ๋ชฉ๋ก ์กฐํ, ๋ฑ๋ก, ์์ , ์ญ์ - SQLite ๋ฐ์ดํฐ๋ฒ ์ด์ค: ๊ฐ๋จํ ๋ง์ด๊ทธ๋ ์ด์ ์ผ๋ก tickets ํ ์ด๋ธ ์์ฑ
- ๊ฒ์ ๊ธฐ๋ฅ: ๊ณต์ฐ๋ช ์ผ๋ก ํฐ์ผ ๊ฒ์
- ์ํ ๊ด๋ฆฌ: ์๋ ์ค/์๋ ์๋ฃ ์ํ ํ ๊ธ
ํ๋ก ํธ์๋ (React 19 + Vite)
- ํฐ์ผ ๋ชฉ๋ก ํ๋ฉด: ์นด๋ ๋ ์ด์์์ผ๋ก ์๋ ํฐ์ผ ๋ชฉ๋ก ํ์
- ํฐ์ผ ๋ฑ๋ก ํผ: ๋ชจ๋ฌ ๋๋ ๋ณ๋ ํ์ด์ง์์ ์๋ ๊ธ ์์ฑ
- ๊ฒ์ ๋ฐ: ์ค์๊ฐ ๊ฒ์์ผ๋ก ์ํ๋ ๊ณต์ฐ ํฐ์ผ ์ฐพ๊ธฐ
- ์ํ ๋ฐฐ์ง: ์๋ ์ค/์๋ ์๋ฃ ์๊ฐ์ ํ์
- ๋ฐ์ํ ๋์์ธ: Tailwind CSS๋ก ๋ชจ๋ฐ์ผ ์นํ์ UI
๐ ์ ํ ์ถ๊ฐ ๊ธฐ๋ฅ (๋ณด๋์ค)
- ํฐ์ผ ์ด๋ฏธ์ง ์ ๋ก๋ (์ข์ ์ ๋ณด ์บก์ฒ ๋ฑ)
- ์นดํ ๊ณ ๋ฆฌ ํํฐ๋ง (๋ฎค์ง์ปฌ, ์ฝ์ํธ, ์ฐ๊ทน ๋ฑ)
- ๊ฐ๊ฒฉ ๋ฒ์ ํํฐ
- ์ข์์/์ฐํ๊ธฐ ๊ธฐ๋ฅ
- ์ต์ ์/๊ฐ๊ฒฉ์ ์ ๋ ฌ
- ๋๊ธ๋ก ๋ฌธ์ํ๊ธฐ
๐จ ๋์์ธ ํฌ์ธํธ
- ํฐ์ผ ์นด๋์ ๊ณต์ฐ ํฌ์คํฐ ๋๋์ ๊ทธ๋ผ๋ฐ์ด์ ๋ฐฐ๊ฒฝ
- "์๋ ์๋ฃ" ํฐ์ผ์ ๋ฐํฌ๋ช ์ฒ๋ฆฌ๋ก ๊ตฌ๋ถ
- ๊ฒ์ ์ ๋ถ๋๋ฌ์ด ์ ๋๋ฉ์ด์ ํจ๊ณผ
- ๋ชจ๋ฐ์ผ์์ ์ค์์ดํ ์ ์ค์ฒ๋ก ์์ธ ์ ๋ณด ํ์ธ
์ ์ฝ ์กฐ๊ฑด
- โข Laravel 13.x์ React 19 ์ต์ ๋ฒ์ ์ฌ์ฉ
- โข ๋ฐฑ์๋๋ RESTful API ์๋ฒ๋ก๋ง ๋์ (๋ณ๋ Laravel ํ๋ก์ ํธ)
- โข ํ๋ก ํธ์๋๋ Vite ๊ธฐ๋ฐ React ํ๋ก์ ํธ (๋ณ๋ ํด๋)
- โข fetch ๋๋ axios๋ก API ํต์
- โข SQLite ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฌ์ฉ (๋ง์ด๊ทธ๋ ์ด์ ํ์)
- โข Tailwind CSS๋ก ์คํ์ผ๋ง
- โข ์ํ ๊ด๋ฆฌ๋ useState๋ง ์ฌ์ฉ (Redux ๋ฑ ๊ธ์ง)
- โข ์ค์๊ฐ ์ฑํ ๊ธฐ๋ฅ ๊ธ์ง (HTTP ๊ธฐ๋ฐ ๋๊ธ๋ก ๋์ฒด ๊ฐ๋ฅ)
ํ๋กฌํํธ ๋ ์ํผ
์๋ ํ๋กฌํํธ๋ฅผ ์์๋๋ก AI์๊ฒ ๋ณด๋ด๋ฉด ๋ฉ๋๋ค. ๋ณต์ฌ ๋ฒํผ์ ๋๋ฌ ๋ฐ๋ก ์ฌ์ฉํ์ธ์.
mkdir concert-ticket-giveaway-board && cd concert-ticket-giveaway-board ์ด ํด๋์์ ํ๋ก์ ํธ๋ฅผ ์์ํฉ๋๋ค. ## Laravel 13.x API ๋ฐฑ์๋ ์ค์ `backend` ํด๋์ Laravel 13.x ํ๋ก์ ํธ๋ฅผ ์์ฑํ๊ณ ์ด๊ธฐ ์ค์ ์ ํด์ฃผ์ธ์: 1. Laravel 13.x ํ๋ก์ ํธ ์์ฑ (composer create-project laravel/laravel backend) 2. .env ํ์ผ์์ DB_CONNECTION=sqlite๋ก ์ค์ 3. database/database.sqlite ํ์ผ ์์ฑ 4. tickets ํ ์ด๋ธ ๋ง์ด๊ทธ๋ ์ด์ ์์ฑ: - ์ปฌ๋ผ: id, title(๊ณต์ฐ๋ช ), date(๊ณต์ฐ๋ ์ง), venue(์ฅ์), quantity(๋งค์), price(๊ฐ๊ฒฉ), contact(์ฐ๋ฝ์ฒ), status(์๋์ค/์๋ฃ, ๊ธฐ๋ณธ๊ฐ '์๋์ค'), description(์์ธ์ค๋ช , nullable), timestamps 5. php artisan migrate ์คํ 6. Ticket ๋ชจ๋ธ ์์ฑ (app/Models/Ticket.php) 7. CORS ์ค์ : config/cors.php์์ 'paths' => ['api/*']๋ก ์ค์ ํ๊ณ , 'supports_credentials' => true ์ถ๊ฐ ์๋ฃ ํ `php artisan serve`๋ก ์๋ฒ ์คํ ํ์ธ (http://localhost:8000)
๊ธฐ๋ ๊ฒฐ๊ณผ: backend ํด๋์ Laravel ํ๋ก์ ํธ๊ฐ ์์ฑ๋๊ณ , SQLite ๋ฐ์ดํฐ๋ฒ ์ด์ค์ tickets ํ ์ด๋ธ์ด ๋ง์ด๊ทธ๋ ์ด์ ๋์ด ์์ด์ผ ํฉ๋๋ค. CORS ์ค์ ์ด ์๋ฃ๋์ด ํ๋ก ํธ์๋์์ API ํธ์ถ ๊ฐ๋ฅํ ์ํ์ ๋๋ค.
## Laravel API ๋ผ์ฐํธ ๋ฐ ์ปจํธ๋กค๋ฌ ๊ตฌํ
backend ํ๋ก์ ํธ์์ ํฐ์ผ ์๋ ๊ธ CRUD API๋ฅผ ๊ตฌํํด์ฃผ์ธ์:
1. TicketController ์์ฑ (php artisan make:controller Api/TicketController --api)
2. routes/api.php์ RESTful ๋ผ์ฐํธ ๋ฑ๋ก:
```php
Route::apiResource('tickets', \App\Http\Controllers\Api\TicketController::class);
Route::patch('tickets/{ticket}/toggle-status', [\App\Http\Controllers\Api\TicketController::class, 'toggleStatus']);
Route::get('tickets/search/{keyword}', [\App\Http\Controllers\Api\TicketController::class, 'search']);
```
3. TicketController์ ๋ค์ ๋ฉ์๋ ๊ตฌํ:
- index(): ๋ชจ๋ ํฐ์ผ ๋ชฉ๋ก ๋ฐํ (์ต์ ์ ์ ๋ ฌ)
- store(): ์ ํฐ์ผ ๋ฑ๋ก (validation ํฌํจ: title, date, venue, quantity, price, contact ํ์)
- show($id): ํน์ ํฐ์ผ ์์ธ ์ ๋ณด
- update($id): ํฐ์ผ ์ ๋ณด ์์
- destroy($id): ํฐ์ผ ์ญ์
- toggleStatus($id): ์ํ๋ฅผ '์๋์ค' โ '์๋์๋ฃ' ํ ๊ธ
- search($keyword): ๊ณต์ฐ๋ช
(title)์ผ๋ก ๊ฒ์ (LIKE ์ฟผ๋ฆฌ)
๋ชจ๋ ์๋ต์ JSON ํ์์ผ๋ก ๋ฐํํ๊ณ , ์ ์ ํ HTTP ์ํ ์ฝ๋ ์ฌ์ฉ (201 Created, 204 No Content ๋ฑ)
๊ธฐ๋ ๊ฒฐ๊ณผ: Postman ๋๋ curl๋ก API ํ ์คํธ ์ ํฐ์ผ CRUD ๋ฐ ๊ฒ์, ์ํ ํ ๊ธ์ด ์ ์ ์๋ํด์ผ ํฉ๋๋ค. ์: GET http://localhost:8000/api/tickets, POST http://localhost:8000/api/tickets
## React 19 ํ๋ก ํธ์๋ ํ๋ก์ ํธ ์ค์
ํ๋ก์ ํธ ๋ฃจํธ์ `frontend` ํด๋๋ฅผ ๋ง๋ค๊ณ React 19 + Vite ํ๋ก์ ํธ๋ฅผ ์์ฑํด์ฃผ์ธ์:
1. npm create vite@latest frontend -- --template react
2. cd frontend && npm install
3. Tailwind CSS ์ค์น ๋ฐ ์ค์ :
```bash
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
```
4. tailwind.config.js์์ content ๊ฒฝ๋ก ์ค์ :
```js
content: ["./index.html", "./src/**/*.{js,jsx}"],
```
5. src/index.css์ Tailwind directives ์ถ๊ฐ:
```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```
6. axios ์ค์น: npm install axios
7. src/App.jsx๋ฅผ ์ด๊ธฐํํ๊ณ ๊ธฐ๋ณธ ๋ ์ด์์ ๊ตฌ์ฑ:
- ํค๋: "๐๏ธ ๊ณต์ฐ ํฐ์ผ ์๋ ๊ฒ์ํ" ํ์ดํ
- ๊ฒ์ ๋ฐ ์์ญ
- ํฐ์ผ ๋ชฉ๋ก ์์ญ
- ํฐ์ผ ๋ฑ๋ก ๋ฒํผ (๋ชจ๋ฌ ํธ๋ฆฌ๊ฑฐ)
์๋ฃ ํ npm run dev๋ก ๊ฐ๋ฐ ์๋ฒ ์คํ (http://localhost:5173)
๊ธฐ๋ ๊ฒฐ๊ณผ: frontend ํด๋์ React + Vite ํ๋ก์ ํธ๊ฐ ์์ฑ๋๊ณ , Tailwind CSS๊ฐ ์ ์ฉ๋ ๊ธฐ๋ณธ ๋ ์ด์์์ด ๋ธ๋ผ์ฐ์ ์ ํ์๋์ด์ผ ํฉ๋๋ค.
## ํฐ์ผ ๋ชฉ๋ก ์กฐํ ๋ฐ ์นด๋ UI ๊ตฌํ
frontend ํ๋ก์ ํธ์์ Laravel API๋ก๋ถํฐ ํฐ์ผ ๋ชฉ๋ก์ ๊ฐ์ ธ์ ํ๋ฉด์ ํ์ํด์ฃผ์ธ์:
1. src/App.jsx์์ useState์ useEffect ์ฌ์ฉ:
- tickets ์ํ๋ก ํฐ์ผ ๋ชฉ๋ก ๊ด๋ฆฌ
- useEffect์์ axios.get('http://localhost:8000/api/tickets')๋ก API ํธ์ถ
2. ํฐ์ผ ์นด๋ ์ปดํฌ๋ํธ ๊ตฌํ (๊ทธ๋ฆฌ๋ ๋ ์ด์์):
- ๊ฐ ์นด๋์ ๊ณต์ฐ๋ช
, ๋ ์ง, ์ฅ์, ๋งค์, ๊ฐ๊ฒฉ ํ์
- ์ํ ๋ฐฐ์ง: ์๋ ์ค(์ด๋ก์), ์๋ ์๋ฃ(ํ์, ๋ฐํฌ๋ช
)
- ์นด๋ ํธ๋ฒ ์ ์ด์ง ์ฌ๋ผ์ค๋ ์ ๋๋ฉ์ด์
(hover:scale-105 transition)
3. ์๋ ์๋ฃ๋ ํฐ์ผ์ opacity-50 ํด๋์ค๋ก ์๊ฐ์ ๊ตฌ๋ถ
4. ๋ฐ์ํ ๊ทธ๋ฆฌ๋: grid-cols-1 md:grid-cols-2 lg:grid-cols-3
5. ๋ก๋ฉ ์ํ ์ฒ๋ฆฌ: ๋ฐ์ดํฐ ๋ก๋ฉ ์ค "ํฐ์ผ ๋ถ๋ฌ์ค๋ ์ค..." ๋ฉ์์ง ํ์
Tailwind CSS๋ก ๊น๋ํ ์นด๋ ๋์์ธ (๊ทธ๋ผ๋ฐ์ด์
๋ฐฐ๊ฒฝ, ๋ฅ๊ทผ ๋ชจ์๋ฆฌ, ๊ทธ๋ฆผ์ ํจ๊ณผ)
๊ธฐ๋ ๊ฒฐ๊ณผ: ๋ธ๋ผ์ฐ์ ์์ Laravel API๋ก๋ถํฐ ๊ฐ์ ธ์จ ํฐ์ผ ๋ชฉ๋ก์ด ์นด๋ ํํ๋ก ํ์๋๊ณ , ์ํ์ ๋ฐ๋ผ ์๊ฐ์ ๊ตฌ๋ถ์ด ๋์ด์ผ ํฉ๋๋ค. ๋ฐ์ํ์ผ๋ก ๋ชจ๋ฐ์ผ/ํ๋ธ๋ฆฟ/๋ฐ์คํฌํฑ์์ ์ ์ ํ ๋ ์ด์์์ด ์ ์ฉ๋ฉ๋๋ค.
## ํฐ์ผ ๋ฑ๋ก ํผ ๋ฐ ๊ฒ์ ๊ธฐ๋ฅ ๊ตฌํ
frontend ํ๋ก์ ํธ์์ ์ ํฐ์ผ ๋ฑ๋ก ํผ๊ณผ ๊ฒ์ ๊ธฐ๋ฅ์ ์ถ๊ฐํด์ฃผ์ธ์:
1. ํฐ์ผ ๋ฑ๋ก ๋ชจ๋ฌ ๊ตฌํ:
- useState๋ก ๋ชจ๋ฌ ์ด๋ฆผ/๋ซํ ์ํ ๊ด๋ฆฌ
- ํผ ํ๋: ๊ณต์ฐ๋ช
, ๋ ์ง(date input), ์ฅ์, ๋งค์(number input), ๊ฐ๊ฒฉ(number input), ์ฐ๋ฝ์ฒ, ์์ธ์ค๋ช
(textarea)
- ํผ ์ ์ถ ์ axios.post('http://localhost:8000/api/tickets', formData)๋ก API ํธ์ถ
- ๋ฑ๋ก ์ฑ๊ณต ์ ๋ชจ๋ฌ ๋ซ๊ณ ํฐ์ผ ๋ชฉ๋ก ์๋ก๊ณ ์นจ
2. ๊ฒ์ ๊ธฐ๋ฅ ๊ตฌํ:
- ๊ฒ์์ด ์
๋ ฅ ํ๋ (์๋จ์ ๋ฐฐ์น)
- ๊ฒ์ ๋ฒํผ ํด๋ฆญ ์ axios.get(`http://localhost:8000/api/tickets/search/${keyword}`)๋ก ๊ฒ์ ๊ฒฐ๊ณผ ๊ฐ์ ธ์ค๊ธฐ
- ๊ฒ์์ด๊ฐ ๋น์ด์์ผ๋ฉด ์ ์ฒด ๋ชฉ๋ก ํ์
3. ์ํ ํ ๊ธ ๋ฒํผ:
- ๊ฐ ํฐ์ผ ์นด๋์ "์๋ ์๋ฃ" ๋ฒํผ ์ถ๊ฐ
- ํด๋ฆญ ์ axios.patch(`http://localhost:8000/api/tickets/${id}/toggle-status`)๋ก ์ํ ๋ณ๊ฒฝ
- ์ํ ๋ณ๊ฒฝ ํ ๋ชฉ๋ก ์๋ก๊ณ ์นจ
๋ชจ๋ฌ์ Tailwind CSS๋ก ์ค์ ์ ๋ ฌ, ๋ฐฐ๊ฒฝ ์ด๋ก๊ฒ ์ฒ๋ฆฌ (backdrop)
๊ธฐ๋ ๊ฒฐ๊ณผ: ํฐ์ผ ๋ฑ๋ก ๋ชจ๋ฌ์ด ์ ์ ์๋ํ๊ณ , ์ ํฐ์ผ์ด ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅ๋ ํ ๋ชฉ๋ก์ ํ์๋์ด์ผ ํฉ๋๋ค. ๊ฒ์ ๊ธฐ๋ฅ์ผ๋ก ๊ณต์ฐ๋ช ํํฐ๋ง์ด ๊ฐ๋ฅํ๊ณ , ์ํ ํ ๊ธ ๋ฒํผ์ผ๋ก ์๋ ์๋ฃ ์ฒ๋ฆฌ๊ฐ ๋์ด์ผ ํฉ๋๋ค.
์ ์ถ๋ ์ํ (0)
๋ก๊ทธ์ธ ํ ์ ์ถ์์ง ์ ์ถ๋ ์ํ์ด ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ ๋์ ์๊ฐ ๋์ด๋ณด์ธ์!