๐ŸŽฎ ์ธ๋”” ๊ฒŒ์ž„ ์Šคํฌ๋ฆฐ์ƒท ๊ฐค๋Ÿฌ๋ฆฌ - ํ”Œ๋ ˆ์ด ์ˆœ๊ฐ„ ์•„์นด์ด๋ธŒ - ๋ฐ”์ด๋ธŒ์ฝ”๋”ฉ ๋ ˆ์‹œํ”ผ
์ž…๋ฌธ PHP + JavaScript Laravel + React AI: Claude

๐ŸŽฎ ์ธ๋”” ๊ฒŒ์ž„ ์Šคํฌ๋ฆฐ์ƒท ๊ฐค๋Ÿฌ๋ฆฌ - ํ”Œ๋ ˆ์ด ์ˆœ๊ฐ„ ์•„์นด์ด๋ธŒ

๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋ช… indie-game-screenshot-gallery
์กฐํšŒ์ˆ˜ 52 0๊ฑด ์ œ์ถœ

๋ฌธ์ œ ์„ค๋ช…

๐ŸŽฎ ์ธ๋”” ๊ฒŒ์ž„ ์Šคํฌ๋ฆฐ์ƒท ๊ฐค๋Ÿฌ๋ฆฌ

๊ฒŒ์ด๋จธ๋“ค์ด ์ข‹์•„ํ•˜๋Š” ์ธ๋”” ๊ฒŒ์ž„์˜ ๋ฉ‹์ง„ ์ˆœ๊ฐ„์„ ์Šคํฌ๋ฆฐ์ƒท์œผ๋กœ ๋‚จ๊ธฐ๊ณ  ๊ณต์œ ํ•˜๋Š” ๊ฐค๋Ÿฌ๋ฆฌ ํ”Œ๋žซํผ์ž…๋‹ˆ๋‹ค. Steam, Epic Games ๋“ฑ์—์„œ ์บก์ฒ˜ํ•œ ๊ฒŒ์ž„ ์Šคํฌ๋ฆฐ์ƒท์„ ์—…๋กœ๋“œํ•˜๊ณ , ๊ฒŒ์ž„๋ณ„๋กœ ๋ถ„๋ฅ˜ํ•˜์—ฌ ๋‚˜๋งŒ์˜ ๊ฒŒ์ž„ ํ”Œ๋ ˆ์ด ์•„์นด์ด๋ธŒ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ•„์ˆ˜ ๊ธฐ๋Šฅ

  • ๊ฒŒ์ž„ ์Šคํฌ๋ฆฐ์ƒท ์—…๋กœ๋“œ (์ด๋ฏธ์ง€ ํŒŒ์ผ)
  • ๊ฒŒ์ž„ ์ œ๋ชฉ, ์žฅ๋ฅด, ํ”Œ๋ ˆ์ด ๋‚ ์งœ ์ž…๋ ฅ
  • ์Šคํฌ๋ฆฐ์ƒท ๋ชฉ๋ก ๊ทธ๋ฆฌ๋“œ ๋ ˆ์ด์•„์›ƒ์œผ๋กœ ํ‘œ์‹œ
  • ๊ฒŒ์ž„๋ณ„ ํ•„ํ„ฐ๋ง (๋“œ๋กญ๋‹ค์šด ์„ ํƒ)
  • ์Šคํฌ๋ฆฐ์ƒท ์ƒ์„ธ ๋ณด๊ธฐ ๋ชจ๋‹ฌ (ํฐ ์ด๋ฏธ์ง€ + ๋ฉ”ํƒ€ ์ •๋ณด)
  • ์ข‹์•„์š” ๋ฒ„ํŠผ (ํ•˜ํŠธ ์•„์ด์ฝ˜, ํด๋ฆญ ์‹œ ์นด์šดํŠธ ์ฆ๊ฐ€)
  • ๋ฐ˜์‘ํ˜• ๋””์ž์ธ (๋ชจ๋ฐ”์ผ/ํƒœ๋ธ”๋ฆฟ/๋ฐ์Šคํฌํ†ฑ)

๋ณด๋„ˆ์Šค ๊ธฐ๋Šฅ

  • ์žฅ๋ฅด๋ณ„ ํ•„ํ„ฐ๋ง ์ถ”๊ฐ€ (์•ก์…˜, RPG, ํผ์ฆ ๋“ฑ)
  • ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ (๊ฒŒ์ž„ ์ œ๋ชฉ์œผ๋กœ ๊ฒ€์ƒ‰)
  • ์Šคํฌ๋ฆฐ์ƒท ์ •๋ ฌ (์ตœ์‹ ์ˆœ/์ข‹์•„์š”์ˆœ)
  • ์ด๋ฏธ์ง€ lazy loading์œผ๋กœ ์„ฑ๋Šฅ ์ตœ์ ํ™”
  • ๋‹คํฌ ๋ชจ๋“œ ์ง€์›

๊ธฐ์ˆ  ์Šคํƒ

  • ๋ฐฑ์—”๋“œ: Laravel 13.x (RESTful API)
  • ํ”„๋ก ํŠธ์—”๋“œ: React 19 + Vite
  • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค: SQLite
  • ์Šคํƒ€์ผ๋ง: Tailwind CSS
  • ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ: Laravel Storage (public disk)

๊ฒŒ์ด๋จธ๋“ค์ด ์ž์‹ ์˜ ๊ฒŒ์ž„ ํ”Œ๋ ˆ์ด ํ•˜์ด๋ผ์ดํŠธ๋ฅผ ์‹œ๊ฐ์ ์œผ๋กœ ๊ธฐ๋กํ•˜๊ณ  ๊ณต์œ ํ•  ์ˆ˜ ์žˆ๋Š” ์‹ค์šฉ์ ์ธ ์„œ๋น„์Šค์ž…๋‹ˆ๋‹ค.

์ œ์•ฝ ์กฐ๊ฑด

  • โ€ข Laravel 13.x ์ตœ์‹  ๋ฒ„์ „ ์‚ฌ์šฉ
  • โ€ข React 19 ์ตœ์‹  ๋ฒ„์ „ ์‚ฌ์šฉ
  • โ€ข SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์‚ฌ์šฉ (๊ฐ„ํŽธํ•œ ๋กœ์ปฌ ๊ฐœ๋ฐœ)
  • โ€ข Tailwind CSS๋กœ ์Šคํƒ€์ผ๋ง
  • โ€ข Laravel Storage๋กœ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ์ฒ˜๋ฆฌ
  • โ€ข fetch API๋กœ Laravel API ํ˜ธ์ถœ
  • โ€ข CORS ์„ค์ • ํ•„์ˆ˜ (Laravel API โ†” React ํ”„๋ก ํŠธ์—”๋“œ)
  • โ€ข ๋ฐ˜์‘ํ˜• ๋””์ž์ธ ์ ์šฉ
  • โ€ข useState๋งŒ ์‚ฌ์šฉ (๋ณต์žกํ•œ ์ƒํƒœ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๊ธˆ์ง€)

ํ”„๋กฌํ”„ํŠธ ๋ ˆ์‹œํ”ผ

์•„๋ž˜ ํ”„๋กฌํ”„ํŠธ๋ฅผ ์ˆœ์„œ๋Œ€๋กœ AI์—๊ฒŒ ๋ณด๋‚ด๋ฉด ๋ฉ๋‹ˆ๋‹ค. ๋ณต์‚ฌ ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ ๋ฐ”๋กœ ์‚ฌ์šฉํ•˜์„ธ์š”.

1
Step 1
mkdir indie-game-screenshot-gallery && cd indie-game-screenshot-gallery

์ด ํด๋”์—์„œ ํ”„๋กœ์ ํŠธ๋ฅผ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.

Laravel 13.x ๋ฐฑ์—”๋“œ API ํ”„๋กœ์ ํŠธ๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ๊ธฐ๋ณธ ์„ค์ •์„ ์™„๋ฃŒํ•˜์„ธ์š”.

**์š”๊ตฌ์‚ฌํ•ญ:**
1. `backend` ํด๋”์— Laravel 13.x ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ (`composer create-project laravel/laravel backend`)
2. `.env` ํŒŒ์ผ์—์„œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ SQLite๋กœ ์„ค์ •:
   ```
   DB_CONNECTION=sqlite
   # DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORD ์ฃผ์„ ์ฒ˜๋ฆฌ
   ```
3. `database/database.sqlite` ํŒŒ์ผ ์ƒ์„ฑ (๋นˆ ํŒŒ์ผ)
4. CORS ์„ค์ •:
   - `config/cors.php`์—์„œ `'supports_credentials' => true` ์„ค์ •
   - `'allowed_origins'`์— `['http://localhost:5173']` ์ถ”๊ฐ€ (React Vite ๊ธฐ๋ณธ ํฌํŠธ)
5. Storage ์„ค์ •:
   - `php artisan storage:link` ๋ช…๋ น์œผ๋กœ public ์Šคํ† ๋ฆฌ์ง€ ์‹ฌ๋ณผ๋ฆญ ๋งํฌ ์ƒ์„ฑ
   - `config/filesystems.php`์—์„œ `'default' => 'public'` ํ™•์ธ

**๋ฐฑ์—”๋“œ ํด๋” ๊ตฌ์กฐ ์˜ˆ์‹œ:**
```
backend/
โ”œโ”€โ”€ app/
โ”œโ”€โ”€ config/
โ”œโ”€โ”€ database/
โ”‚   โ””โ”€โ”€ database.sqlite
โ”œโ”€โ”€ public/
โ”‚   โ””โ”€โ”€ storage/ (์‹ฌ๋ณผ๋ฆญ ๋งํฌ)
โ”œโ”€โ”€ routes/
โ””โ”€โ”€ .env
```

**ํ™•์ธ ๋ฐฉ๋ฒ•:**
- `php artisan serve`๋กœ Laravel ์„œ๋ฒ„ ์‹คํ–‰ (http://localhost:8000)
- ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ ‘์† ์‹œ Laravel ๊ธฐ๋ณธ ํŽ˜์ด์ง€ ํ‘œ์‹œ
- `php artisan route:list`๋กœ ๋ผ์šฐํŠธ ๋ชฉ๋ก ํ™•์ธ

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: Laravel 13.x ๋ฐฑ์—”๋“œ ํ”„๋กœ์ ํŠธ๊ฐ€ `backend` ํด๋”์— ์ƒ์„ฑ๋˜๊ณ , SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ • ๋ฐ CORS ์„ค์ •์ด ์™„๋ฃŒ๋ฉ๋‹ˆ๋‹ค. Storage ์‹ฌ๋ณผ๋ฆญ ๋งํฌ๊ฐ€ ์ƒ์„ฑ๋˜์–ด ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ์ค€๋น„๊ฐ€ ์™„๋ฃŒ๋ฉ๋‹ˆ๋‹ค.

2
Step 2
Laravel ๋ฐฑ์—”๋“œ์—์„œ ์Šคํฌ๋ฆฐ์ƒท ๊ด€๋ฆฌ API๋ฅผ ๊ตฌ์ถ•ํ•˜์„ธ์š”.

**์š”๊ตฌ์‚ฌํ•ญ:**

### 1. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ƒ์„ฑ
`php artisan make:migration create_screenshots_table`๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ ์ƒ์„ฑ ํ›„ ๋‹ค์Œ ์Šคํ‚ค๋งˆ ์ •์˜:
```php
Schema::create('screenshots', function (Blueprint $table) {
    $table->id();
    $table->string('game_title'); // ๊ฒŒ์ž„ ์ œ๋ชฉ
    $table->string('genre'); // ์žฅ๋ฅด (Action, RPG, Puzzle ๋“ฑ)
    $table->date('play_date'); // ํ”Œ๋ ˆ์ด ๋‚ ์งœ
    $table->string('image_path'); // ์ด๋ฏธ์ง€ ํŒŒ์ผ ๊ฒฝ๋กœ
    $table->integer('likes')->default(0); // ์ข‹์•„์š” ์ˆ˜
    $table->timestamps();
});
```

### 2. ๋ชจ๋ธ ์ƒ์„ฑ
`php artisan make:model Screenshot`๋กœ ๋ชจ๋ธ ์ƒ์„ฑ ํ›„ fillable ํ•„๋“œ ์„ค์ •:
```php
protected $fillable = ['game_title', 'genre', 'play_date', 'image_path', 'likes'];
```

### 3. API ์ปจํŠธ๋กค๋Ÿฌ ์ƒ์„ฑ
`php artisan make:controller Api/ScreenshotController`๋กœ ์ปจํŠธ๋กค๋Ÿฌ ์ƒ์„ฑ ํ›„ ๋‹ค์Œ ๋ฉ”์„œ๋“œ ๊ตฌํ˜„:

- `index()`: ๋ชจ๋“  ์Šคํฌ๋ฆฐ์ƒท ๋ชฉ๋ก ๋ฐ˜ํ™˜ (JSON)
  - ์ตœ์‹ ์ˆœ ์ •๋ ฌ (`orderBy('created_at', 'desc')`)
  - ์ด๋ฏธ์ง€ URL์€ `Storage::url($screenshot->image_path)`๋กœ ๋ณ€ํ™˜
  
- `store(Request $request)`: ์ƒˆ ์Šคํฌ๋ฆฐ์ƒท ์—…๋กœ๋“œ
  - ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ:
    ```php
    $request->validate([
        'game_title' => 'required|string|max:255',
        'genre' => 'required|string',
        'play_date' => 'required|date',
        'image' => 'required|image|mimes:jpeg,png,jpg,gif|max:5120', // 5MB
    ]);
    ```
  - ์ด๋ฏธ์ง€ ํŒŒ์ผ ์ €์žฅ: `$request->file('image')->store('screenshots', 'public')`
  - ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅ ํ›„ ์ƒ์„ฑ๋œ ์Šคํฌ๋ฆฐ์ƒท ๋ฐ˜ํ™˜

- `show($id)`: ํŠน์ • ์Šคํฌ๋ฆฐ์ƒท ์ƒ์„ธ ์ •๋ณด ๋ฐ˜ํ™˜

- `like($id)`: ์ข‹์•„์š” ์ฆ๊ฐ€
  - `$screenshot->increment('likes')`๋กœ ์ข‹์•„์š” ์ˆ˜ ์ฆ๊ฐ€
  - ์—…๋ฐ์ดํŠธ๋œ ์Šคํฌ๋ฆฐ์ƒท ๋ฐ˜ํ™˜

### 4. API ๋ผ์šฐํŠธ ๋“ฑ๋ก
`routes/api.php`์— ๋‹ค์Œ ๋ผ์šฐํŠธ ์ถ”๊ฐ€:
```php
use App\Http\Controllers\Api\ScreenshotController;

Route::get('/screenshots', [ScreenshotController::class, 'index']);
Route::post('/screenshots', [ScreenshotController::class, 'store']);
Route::get('/screenshots/{id}', [ScreenshotController::class, 'show']);
Route::post('/screenshots/{id}/like', [ScreenshotController::class, 'like']);
```

### 5. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰
`php artisan migrate`๋กœ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ…Œ์ด๋ธ” ์ƒ์„ฑ

**ํ™•์ธ ๋ฐฉ๋ฒ•:**
- Postman ๋˜๋Š” curl๋กœ API ํ…Œ์ŠคํŠธ:
  - `GET http://localhost:8000/api/screenshots` โ†’ ๋นˆ ๋ฐฐ์—ด ๋ฐ˜ํ™˜
  - `POST http://localhost:8000/api/screenshots` (FormData๋กœ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ) โ†’ ์Šคํฌ๋ฆฐ์ƒท ์ƒ์„ฑ
- `php artisan route:list`๋กœ API ๋ผ์šฐํŠธ ํ™•์ธ

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: Laravel ๋ฐฑ์—”๋“œ์— ์Šคํฌ๋ฆฐ์ƒท CRUD API๊ฐ€ ๊ตฌ์ถ•๋ฉ๋‹ˆ๋‹ค. ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ, ๋ชฉ๋ก ์กฐํšŒ, ์ƒ์„ธ ์กฐํšŒ, ์ข‹์•„์š” ๊ธฐ๋Šฅ์ด ๋ชจ๋‘ ๋™์ž‘ํ•˜๋ฉฐ, Postman์œผ๋กœ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

3
Step 3
React 19 ํ”„๋ก ํŠธ์—”๋“œ ํ”„๋กœ์ ํŠธ๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ๊ธฐ๋ณธ ๋ ˆ์ด์•„์›ƒ์„ ๊ตฌ์„ฑํ•˜์„ธ์š”.

**์š”๊ตฌ์‚ฌํ•ญ:**

### 1. React ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ
ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ์—์„œ ๋‹ค์Œ ๋ช…๋ น ์‹คํ–‰:
```bash
npm create vite@latest frontend -- --template react
cd frontend
npm install
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
```

### 2. Tailwind CSS ์„ค์ •
`tailwind.config.js` ํŒŒ์ผ ์ˆ˜์ •:
```js
export default {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
```

`src/index.css`์— Tailwind ๋””๋ ‰ํ‹ฐ๋ธŒ ์ถ”๊ฐ€:
```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```

### 3. ๊ธฐ๋ณธ ๋ ˆ์ด์•„์›ƒ ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ
`src/App.jsx` ํŒŒ์ผ์„ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ž‘์„ฑ:

```jsx
import { useState } from 'react'

function App() {
  return (
    <div className="min-h-screen bg-gradient-to-br from-purple-900 via-indigo-900 to-blue-900">
      {/* ํ—ค๋” */}
      <header className="bg-black bg-opacity-50 backdrop-blur-md border-b border-purple-500/30">
        <div className="container mx-auto px-4 py-6">
          <h1 className="text-4xl font-bold text-white flex items-center gap-3">
            ๐ŸŽฎ <span>Indie Game Gallery</span>
          </h1>
          <p className="text-purple-300 mt-2">Share your epic gaming moments</p>
        </div>
      </header>

      {/* ๋ฉ”์ธ ์ปจํ…์ธ  */}
      <main className="container mx-auto px-4 py-8">
        {/* ์—…๋กœ๋“œ ํผ ์˜์—ญ (๋‹ค์Œ ๋‹จ๊ณ„์—์„œ ๊ตฌํ˜„) */}
        <section className="bg-white bg-opacity-10 backdrop-blur-md rounded-2xl p-6 mb-8 border border-purple-500/30">
          <h2 className="text-2xl font-bold text-white mb-4">๐Ÿ“ธ Upload Screenshot</h2>
          <p className="text-purple-200">Upload form will be added here...</p>
        </section>

        {/* ๊ฐค๋Ÿฌ๋ฆฌ ๊ทธ๋ฆฌ๋“œ ์˜์—ญ (๋‹ค์Œ ๋‹จ๊ณ„์—์„œ ๊ตฌํ˜„) */}
        <section>
          <h2 className="text-2xl font-bold text-white mb-6">๐Ÿ–ผ๏ธ Gallery</h2>
          <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
            <p className="text-purple-200 col-span-full text-center py-12">No screenshots yet. Upload your first one!</p>
          </div>
        </section>
      </main>

      {/* ํ‘ธํ„ฐ */}
      <footer className="bg-black bg-opacity-50 backdrop-blur-md border-t border-purple-500/30 mt-16">
        <div className="container mx-auto px-4 py-6 text-center text-purple-300">
          <p>Built with Laravel 13 + React 19 ๐Ÿš€</p>
        </div>
      </footer>
    </div>
  )
}

export default App
```

### 4. ๊ฐœ๋ฐœ ์„œ๋ฒ„ ์‹คํ–‰
`npm run dev`๋กœ React ๊ฐœ๋ฐœ ์„œ๋ฒ„ ์‹คํ–‰ (http://localhost:5173)

**ํ™•์ธ ๋ฐฉ๋ฒ•:**
- ๋ธŒ๋ผ์šฐ์ €์—์„œ http://localhost:5173 ์ ‘์†
- ๊ฒŒ์ž„ ๊ฐค๋Ÿฌ๋ฆฌ ํ—ค๋”์™€ ๊ธฐ๋ณธ ๋ ˆ์ด์•„์›ƒ์ด ํ‘œ์‹œ๋จ
- ๋‹คํฌ ํ…Œ๋งˆ์˜ ๊ฒŒ์ด๋ฐ ๋ถ„์œ„๊ธฐ ๋””์ž์ธ ํ™•์ธ

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: React 19 ํ”„๋ก ํŠธ์—”๋“œ ํ”„๋กœ์ ํŠธ๊ฐ€ ์ƒ์„ฑ๋˜๊ณ  Tailwind CSS๊ฐ€ ์„ค์ •๋ฉ๋‹ˆ๋‹ค. ๊ฒŒ์ž„ ๊ฐค๋Ÿฌ๋ฆฌ ํ—ค๋”, ์—…๋กœ๋“œ ํผ ์„น์…˜ ํ”Œ๋ ˆ์ด์Šคํ™€๋”, ๊ฐค๋Ÿฌ๋ฆฌ ๊ทธ๋ฆฌ๋“œ ํ”Œ๋ ˆ์ด์Šคํ™€๋”๊ฐ€ ํฌํ•จ๋œ ๊ธฐ๋ณธ ๋ ˆ์ด์•„์›ƒ์ด ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.

4
Step 4
React ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ์Šคํฌ๋ฆฐ์ƒท ์—…๋กœ๋“œ ํผ๊ณผ ๊ฐค๋Ÿฌ๋ฆฌ ๋ชฉ๋ก์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.

**์š”๊ตฌ์‚ฌํ•ญ:**

### 1. `src/App.jsx` ์ˆ˜์ •

```jsx
import { useState, useEffect } from 'react'

const API_BASE_URL = 'http://localhost:8000/api'

function App() {
  const [screenshots, setScreenshots] = useState([])
  const [loading, setLoading] = useState(true)
  const [formData, setFormData] = useState({
    game_title: '',
    genre: '',
    play_date: '',
    image: null
  })
  const [uploading, setUploading] = useState(false)

  // ์Šคํฌ๋ฆฐ์ƒท ๋ชฉ๋ก ๋ถˆ๋Ÿฌ์˜ค๊ธฐ
  useEffect(() => {
    fetchScreenshots()
  }, [])

  const fetchScreenshots = async () => {
    try {
      const response = await fetch(`${API_BASE_URL}/screenshots`)
      const data = await response.json()
      setScreenshots(data)
    } catch (error) {
      console.error('Failed to fetch screenshots:', error)
    } finally {
      setLoading(false)
    }
  }

  // ํผ ์ž…๋ ฅ ์ฒ˜๋ฆฌ
  const handleInputChange = (e) => {
    const { name, value } = e.target
    setFormData(prev => ({ ...prev, [name]: value }))
  }

  const handleFileChange = (e) => {
    setFormData(prev => ({ ...prev, image: e.target.files[0] }))
  }

  // ์—…๋กœ๋“œ ์ฒ˜๋ฆฌ
  const handleSubmit = async (e) => {
    e.preventDefault()
    setUploading(true)

    const formDataToSend = new FormData()
    formDataToSend.append('game_title', formData.game_title)
    formDataToSend.append('genre', formData.genre)
    formDataToSend.append('play_date', formData.play_date)
    formDataToSend.append('image', formData.image)

    try {
      const response = await fetch(`${API_BASE_URL}/screenshots`, {
        method: 'POST',
        body: formDataToSend
      })

      if (response.ok) {
        // ํผ ์ดˆ๊ธฐํ™”
        setFormData({ game_title: '', genre: '', play_date: '', image: null })
        document.getElementById('image-upload').value = ''
        // ๋ชฉ๋ก ์ƒˆ๋กœ๊ณ ์นจ
        fetchScreenshots()
        alert('Screenshot uploaded successfully! ๐ŸŽ‰')
      }
    } catch (error) {
      console.error('Upload failed:', error)
      alert('Upload failed. Please try again.')
    } finally {
      setUploading(false)
    }
  }

  // ์ข‹์•„์š” ์ฒ˜๋ฆฌ
  const handleLike = async (id) => {
    try {
      const response = await fetch(`${API_BASE_URL}/screenshots/${id}/like`, {
        method: 'POST'
      })
      if (response.ok) {
        fetchScreenshots() // ๋ชฉ๋ก ์ƒˆ๋กœ๊ณ ์นจ
      }
    } catch (error) {
      console.error('Like failed:', error)
    }
  }

  return (
    <div className="min-h-screen bg-gradient-to-br from-purple-900 via-indigo-900 to-blue-900">
      <header className="bg-black bg-opacity-50 backdrop-blur-md border-b border-purple-500/30">
        <div className="container mx-auto px-4 py-6">
          <h1 className="text-4xl font-bold text-white flex items-center gap-3">
            ๐ŸŽฎ <span>Indie Game Gallery</span>
          </h1>
          <p className="text-purple-300 mt-2">Share your epic gaming moments</p>
        </div>
      </header>

      <main className="container mx-auto px-4 py-8">
        {/* ์—…๋กœ๋“œ ํผ */}
        <section className="bg-white bg-opacity-10 backdrop-blur-md rounded-2xl p-6 mb-8 border border-purple-500/30">
          <h2 className="text-2xl font-bold text-white mb-4">๐Ÿ“ธ Upload Screenshot</h2>
          <form onSubmit={handleSubmit} className="space-y-4">
            <div>
              <label className="block text-purple-200 mb-2">Game Title</label>
              <input
                type="text"
                name="game_title"
                value={formData.game_title}
                onChange={handleInputChange}
                required
                className="w-full px-4 py-2 rounded-lg bg-white bg-opacity-20 border border-purple-400 text-white placeholder-purple-300 focus:outline-none focus:ring-2 focus:ring-purple-500"
                placeholder="e.g., Hollow Knight"
              />
            </div>
            <div>
              <label className="block text-purple-200 mb-2">Genre</label>
              <select
                name="genre"
                value={formData.genre}
                onChange={handleInputChange}
                required
                className="w-full px-4 py-2 rounded-lg bg-white bg-opacity-20 border border-purple-400 text-white focus:outline-none focus:ring-2 focus:ring-purple-500"
              >
                <option value="">Select Genre</option>
                <option value="Action">Action</option>
                <option value="RPG">RPG</option>
                <option value="Puzzle">Puzzle</option>
                <option value="Adventure">Adventure</option>
                <option value="Platformer">Platformer</option>
                <option value="Strategy">Strategy</option>
              </select>
            </div>
            <div>
              <label className="block text-purple-200 mb-2">Play Date</label>
              <input
                type="date"
                name="play_date"
                value={formData.play_date}
                onChange={handleInputChange}
                required
                className="w-full px-4 py-2 rounded-lg bg-white bg-opacity-20 border border-purple-400 text-white focus:outline-none focus:ring-2 focus:ring-purple-500"
              />
            </div>
            <div>
              <label className="block text-purple-200 mb-2">Screenshot Image</label>
              <input
                id="image-upload"
                type="file"
                accept="image/*"
                onChange={handleFileChange}
                required
                className="w-full px-4 py-2 rounded-lg bg-white bg-opacity-20 border border-purple-400 text-white file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:bg-purple-600 file:text-white hover:file:bg-purple-700"
              />
            </div>
            <button
              type="submit"
              disabled={uploading}
              className="w-full bg-purple-600 hover:bg-purple-700 text-white font-bold py-3 px-6 rounded-lg transition disabled:opacity-50 disabled:cursor-not-allowed"
            >
              {uploading ? 'โณ Uploading...' : '๐Ÿš€ Upload Screenshot'}
            </button>
          </form>
        </section>

        {/* ๊ฐค๋Ÿฌ๋ฆฌ ๊ทธ๋ฆฌ๋“œ */}
        <section>
          <h2 className="text-2xl font-bold text-white mb-6">๐Ÿ–ผ๏ธ Gallery</h2>
          {loading ? (
            <p className="text-purple-200 text-center py-12">Loading...</p>
          ) : screenshots.length === 0 ? (
            <p className="text-purple-200 text-center py-12">No screenshots yet. Upload your first one!</p>
          ) : (
            <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
              {screenshots.map(screenshot => (
                <div key={screenshot.id} className="bg-white bg-opacity-10 backdrop-blur-md rounded-xl overflow-hidden border border-purple-500/30 hover:border-purple-400 transition">
                  <img
                    src={`http://localhost:8000${screenshot.image_url}`}
                    alt={screenshot.game_title}
                    className="w-full h-64 object-cover"
                  />
                  <div className="p-4">
                    <h3 className="text-xl font-bold text-white mb-2">{screenshot.game_title}</h3>
                    <div className="flex items-center gap-2 text-sm text-purple-300 mb-3">
                      <span className="bg-purple-600 px-2 py-1 rounded">{screenshot.genre}</span>
                      <span>๐Ÿ“… {screenshot.play_date}</span>
                    </div>
                    <button
                      onClick={() => handleLike(screenshot.id)}
                      className="flex items-center gap-2 text-pink-400 hover:text-pink-300 transition"
                    >
                      โค๏ธ <span>{screenshot.likes}</span>
                    </button>
                  </div>
                </div>
              ))}
            </div>
          )}
        </section>
      </main>

      <footer className="bg-black bg-opacity-50 backdrop-blur-md border-t border-purple-500/30 mt-16">
        <div className="container mx-auto px-4 py-6 text-center text-purple-300">
          <p>Built with Laravel 13 + React 19 ๐Ÿš€</p>
        </div>
      </footer>
    </div>
  )
}

export default App
```

**ํ™•์ธ ๋ฐฉ๋ฒ•:**
1. Laravel ๋ฐฑ์—”๋“œ ์„œ๋ฒ„ ์‹คํ–‰ (`php artisan serve`)
2. React ํ”„๋ก ํŠธ์—”๋“œ ์„œ๋ฒ„ ์‹คํ–‰ (`npm run dev`)
3. ๋ธŒ๋ผ์šฐ์ €์—์„œ http://localhost:5173 ์ ‘์†
4. ์—…๋กœ๋“œ ํผ์—์„œ ๊ฒŒ์ž„ ์ •๋ณด์™€ ์Šคํฌ๋ฆฐ์ƒท ์ด๋ฏธ์ง€ ์ž…๋ ฅ ํ›„ ์—…๋กœ๋“œ
5. ๊ฐค๋Ÿฌ๋ฆฌ ๊ทธ๋ฆฌ๋“œ์— ์Šคํฌ๋ฆฐ์ƒท ์นด๋“œ๊ฐ€ ํ‘œ์‹œ๋˜๋Š”์ง€ ํ™•์ธ
6. ์ข‹์•„์š” ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ์นด์šดํŠธ๊ฐ€ ์ฆ๊ฐ€ํ•˜๋Š”์ง€ ํ™•์ธ

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: React ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ์Šคํฌ๋ฆฐ์ƒท ์—…๋กœ๋“œ ํผ๊ณผ ๊ฐค๋Ÿฌ๋ฆฌ ๋ชฉ๋ก์ด ์™„์„ฑ๋ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๋Š” ๊ฒŒ์ž„ ์ œ๋ชฉ, ์žฅ๋ฅด, ํ”Œ๋ ˆ์ด ๋‚ ์งœ, ์ด๋ฏธ์ง€๋ฅผ ์ž…๋ ฅํ•˜์—ฌ ์Šคํฌ๋ฆฐ์ƒท์„ ์—…๋กœ๋“œํ•  ์ˆ˜ ์žˆ๊ณ , ์—…๋กœ๋“œ๋œ ์Šคํฌ๋ฆฐ์ƒท์ด ๊ทธ๋ฆฌ๋“œ ๋ ˆ์ด์•„์›ƒ์œผ๋กœ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. ์ข‹์•„์š” ๋ฒ„ํŠผ๋„ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.

์ œ์ถœ๋œ ์ž‘ํ’ˆ (0)

๋กœ๊ทธ์ธ ํ›„ ์ œ์ถœ

์•„์ง ์ œ์ถœ๋œ ์ž‘ํ’ˆ์ด ์—†์Šต๋‹ˆ๋‹ค. ์ฒซ ๋ฒˆ์งธ ๋„์ „์ž๊ฐ€ ๋˜์–ด๋ณด์„ธ์š”!

๋Œ“๊ธ€ (0)

๋กœ๊ทธ์ธ ํ›„ ๋Œ“๊ธ€์„ ๋‚จ๊ธธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค. ์ฒซ ๋ฒˆ์งธ ๋Œ“๊ธ€์„ ๋‚จ๊ฒจ๋ณด์„ธ์š”!