๐ŸŽฌ ์˜ํ™” ๊ฐ์ƒ ํ๋ ˆ์ด์…˜ - ๋ณธ ์˜ํ™” ํ‰์  & ์ถ”์ฒœ ๋ฆฌ์ŠคํŠธ - ๋ฐ”์ด๋ธŒ์ฝ”๋”ฉ ๋ ˆ์‹œํ”ผ
์ž…๋ฌธ PHP + JavaScript Laravel + React AI: Claude

๐ŸŽฌ ์˜ํ™” ๊ฐ์ƒ ํ๋ ˆ์ด์…˜ - ๋ณธ ์˜ํ™” ํ‰์  & ์ถ”์ฒœ ๋ฆฌ์ŠคํŠธ

๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋ช… movie-rating-recommendation-list
์กฐํšŒ์ˆ˜ 92 0๊ฑด ์ œ์ถœ

๋ฌธ์ œ ์„ค๋ช…

ํ”„๋กœ์ ํŠธ ๊ฐœ์š”

์˜ํ™”๋ฅผ ์ข‹์•„ํ•˜๋Š” ์‚ฌ๋žŒ๋“ค์„ ์œ„ํ•œ ๊ฐœ์ธ ์˜ํ™” ๊ฐ์ƒ ๊ธฐ๋ก ํ”Œ๋žซํผ์ž…๋‹ˆ๋‹ค. ๋ณธ ์˜ํ™”์— ํ‰์ ๊ณผ ๊ฐ„๋‹จํ•œ ์ฝ”๋ฉ˜ํŠธ๋ฅผ ๋‚จ๊ธฐ๊ณ , ๋ณด๊ณ  ์‹ถ์€ ์˜ํ™”๋ฅผ ์œ„์‹œ๋ฆฌ์ŠคํŠธ์— ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. Laravel API์™€ React ํ”„๋ก ํŠธ์—”๋“œ๋ฅผ ์—ฐ๋™ํ•˜์—ฌ ํ’€์Šคํƒ ๊ฐœ๋ฐœ์˜ ๊ธฐ์ดˆ๋ฅผ ๋ฐฐ์šธ ์ˆ˜ ์žˆ๋Š” ์ž…๋ฌธ์ž์šฉ ํ”„๋กœ์ ํŠธ์ž…๋‹ˆ๋‹ค.

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

  • ์˜ํ™” ๋ชฉ๋ก ํŽ˜์ด์ง€: ๋‚ด๊ฐ€ ๋ณธ ์˜ํ™” ๋ฆฌ์ŠคํŠธ๋ฅผ ์นด๋“œ ํ˜•ํƒœ๋กœ ํ‘œ์‹œ (์ œ๋ชฉ, ํฌ์Šคํ„ฐ ์ด๋ฏธ์ง€, ํ‰์ )
  • ์˜ํ™” ์ถ”๊ฐ€ ํผ: ์˜ํ™” ์ œ๋ชฉ, ํฌ์Šคํ„ฐ URL, ์žฅ๋ฅด, ๊ฐ์ƒ ํ‰์ (1~5์ ), ํ•œ์ค„ํ‰ ์ž…๋ ฅ
  • ํ‰์  ํ‘œ์‹œ: ๋ณ„์  ์•„์ด์ฝ˜์œผ๋กœ ์‹œ๊ฐํ™” (โ˜…โ˜…โ˜…โ˜…โ˜†)
  • ์œ„์‹œ๋ฆฌ์ŠคํŠธ: ๋ณด๊ณ  ์‹ถ์€ ์˜ํ™”๋ฅผ ๋ณ„๋„ ํƒญ/ํŽ˜์ด์ง€๋กœ ๊ด€๋ฆฌ
  • ํ•„ํ„ฐ๋ง: ์žฅ๋ฅด๋ณ„, ํ‰์ ๋ณ„ ํ•„ํ„ฐ ๊ธฐ๋Šฅ
  • ์‚ญ์ œ ๊ธฐ๋Šฅ: ์˜ํ™” ํ•ญ๋ชฉ ์‚ญ์ œ

์„ ํƒ ์ถ”๊ฐ€ ๊ธฐ๋Šฅ (๋ณด๋„ˆ์Šค)

  • ์˜ํ™” ํฌ์Šคํ„ฐ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ๊ธฐ๋Šฅ (Laravel Storage ํ™œ์šฉ)
  • ๊ฐ์ƒ์ผ ๊ธฐ๋ก ๋ฐ ์ตœ์‹ ์ˆœ/์˜ค๋ž˜๋œ์ˆœ ์ •๋ ฌ
  • ์ด ๊ฐ์ƒ ์˜ํ™” ์ˆ˜, ํ‰๊ท  ํ‰์  ํ†ต๊ณ„ ํ‘œ์‹œ
  • ๋‹คํฌ ๋ชจ๋“œ UI
  • ์˜ํ™” ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ (์ œ๋ชฉ ๊ฒ€์ƒ‰)
  • TMDB API ์—ฐ๋™ํ•˜์—ฌ ์˜ํ™” ์ •๋ณด ์ž๋™ ์™„์„ฑ

๊ธฐ์ˆ  ์Šคํƒ

  • ๋ฐฑ์—”๋“œ: Laravel 13.x (RESTful API, SQLite)
  • ํ”„๋ก ํŠธ์—”๋“œ: React 19 (Vite)
  • ์Šคํƒ€์ผ๋ง: Tailwind CSS
  • HTTP ํด๋ผ์ด์–ธํŠธ: Axios

์ œ์•ฝ ์กฐ๊ฑด

  • โ€ข Laravel 13.x ์ตœ์‹  ๋ฒ„์ „ ์‚ฌ์šฉ
  • โ€ข React 19 ์ตœ์‹  ๋ฒ„์ „ ์‚ฌ์šฉ
  • โ€ข Laravel์€ API ์„œ๋ฒ„ ์—ญํ•  (routes/api.php ์‚ฌ์šฉ)
  • โ€ข React๋Š” ๋ณ„๋„ Vite ํ”„๋กœ์ ํŠธ๋กœ ๊ตฌ์„ฑ
  • โ€ข SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์‚ฌ์šฉ (๊ฐ„๋‹จํ•œ ์„ค์ •)
  • โ€ข CORS ์„ค์ • ํ•„์ˆ˜ (Laravel์—์„œ React ์š”์ฒญ ํ—ˆ์šฉ)
  • โ€ข RESTful API ์„ค๊ณ„ ์›์น™ ์ค€์ˆ˜ (GET, POST, DELETE)
  • โ€ข React์—์„œ fetch ๋˜๋Š” axios๋กœ API ํ˜ธ์ถœ
  • โ€ข useState ํ›…๋งŒ ์‚ฌ์šฉ (๋ณต์žกํ•œ ์ƒํƒœ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๊ธˆ์ง€)
  • โ€ข Tailwind CSS๋กœ ๋ฐ˜์‘ํ˜• ๋””์ž์ธ ๊ตฌํ˜„

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

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

1
Step 1
mkdir movie-rating-recommendation-list && cd movie-rating-recommendation-list

์ด ํด๋”์—์„œ Laravel 13.x + React 19 ํ’€์Šคํƒ ํ”„๋กœ์ ํŠธ๋ฅผ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.

**๋ฐฑ์—”๋“œ ์„ค์ •:**

1. Laravel 13.x ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ:
   ```bash
   composer create-project laravel/laravel backend
   cd backend
   ```

2. `.env` ํŒŒ์ผ์—์„œ SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ •:
   ```
   DB_CONNECTION=sqlite
   # DB_HOST, DB_PORT, DB_DATABASE ์ฃผ์„ ์ฒ˜๋ฆฌ
   ```

3. `database/database.sqlite` ํŒŒ์ผ ์ƒ์„ฑ:
   ```bash
   touch database/database.sqlite
   ```

4. ์˜ํ™” ๋ชจ๋ธ ๋ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ƒ์„ฑ:
   ```bash
   php artisan make:model Movie -m
   ```

5. `database/migrations/xxxx_create_movies_table.php` ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ ์ˆ˜์ •:
   ```php
   Schema::create('movies', function (Blueprint $table) {
       $table->id();
       $table->string('title');
       $table->string('poster_url')->nullable();
       $table->string('genre');
       $table->integer('rating'); // 1~5์ 
       $table->text('comment')->nullable();
       $table->boolean('is_wishlist')->default(false); // ์œ„์‹œ๋ฆฌ์ŠคํŠธ ์—ฌ๋ถ€
       $table->timestamps();
   });
   ```

6. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰:
   ```bash
   php artisan migrate
   ```

7. CORS ์„ค์ • (`config/cors.php`):
   ```php
   'paths' => ['api/*'],
   'allowed_origins' => ['http://localhost:5173'],
   'allowed_methods' => ['*'],
   'allowed_headers' => ['*'],
   ```

8. API ๋ผ์šฐํŠธ ์„ค์ • (`routes/api.php`):
   ```php
   use App\Http\Controllers\MovieController;

   Route::get('/movies', [MovieController::class, 'index']);
   Route::post('/movies', [MovieController::class, 'store']);
   Route::delete('/movies/{id}', [MovieController::class, 'destroy']);
   ```

9. MovieController ์ƒ์„ฑ ๋ฐ ๊ตฌํ˜„:
   ```bash
   php artisan make:controller MovieController
   ```

   `app/Http/Controllers/MovieController.php`:
   ```php
   namespace App\Http\Controllers;

   use App\Models\Movie;
   use Illuminate\Http\Request;

   class MovieController extends Controller
   {
       public function index()
       {
           return Movie::orderBy('created_at', 'desc')->get();
       }

       public function store(Request $request)
       {
           $validated = $request->validate([
               'title' => 'required|string|max:255',
               'poster_url' => 'nullable|url',
               'genre' => 'required|string',
               'rating' => 'required|integer|min:1|max:5',
               'comment' => 'nullable|string',
               'is_wishlist' => 'boolean',
           ]);

           return Movie::create($validated);
       }

       public function destroy($id)
       {
           Movie::findOrFail($id)->delete();
           return response()->json(['message' => 'Movie deleted']);
       }
   }
   ```

10. `app/Models/Movie.php`์— fillable ์ถ”๊ฐ€:
    ```php
    protected $fillable = ['title', 'poster_url', 'genre', 'rating', 'comment', 'is_wishlist'];
    ```

11. Laravel ๊ฐœ๋ฐœ ์„œ๋ฒ„ ์‹คํ–‰:
    ```bash
    php artisan serve
    ```
    (๊ธฐ๋ณธ ํฌํŠธ: http://localhost:8000)

๋ฐฑ์—”๋“œ API๊ฐ€ ์ค€๋น„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ ๋‹จ๊ณ„์—์„œ React ํ”„๋ก ํŠธ์—”๋“œ๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: Laravel ๋ฐฑ์—”๋“œ๊ฐ€ http://localhost:8000์—์„œ ์‹คํ–‰๋˜๋ฉฐ, /api/movies GET/POST/DELETE ์—”๋“œํฌ์ธํŠธ๊ฐ€ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค. SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— movies ํ…Œ์ด๋ธ”์ด ์ƒ์„ฑ๋˜๊ณ , CORS๊ฐ€ ์„ค์ •๋˜์–ด React์—์„œ API๋ฅผ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

2
Step 2
**ํ”„๋ก ํŠธ์—”๋“œ ์„ค์ •:**

ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ ํด๋”(movie-rating-recommendation-list)๋กœ ๋Œ์•„์™€์„œ React ํ”„๋ก ํŠธ์—”๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

1. React + Vite ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ:
   ```bash
   cd ..
   npm create vite@latest frontend -- --template react
   cd frontend
   npm install
   ```

2. Axios ๋ฐ Tailwind CSS ์„ค์น˜:
   ```bash
   npm install axios
   npm install -D tailwindcss postcss autoprefixer
   npx tailwindcss init -p
   ```

3. `tailwind.config.js` ์ˆ˜์ •:
   ```js
   export default {
     content: [
       "./index.html",
       "./src/**/*.{js,ts,jsx,tsx}",
     ],
     theme: {
       extend: {},
     },
     plugins: [],
   }
   ```

4. `src/index.css` ํŒŒ์ผ์— Tailwind ์ง€์‹œ๋ฌธ ์ถ”๊ฐ€:
   ```css
   @tailwind base;
   @tailwind components;
   @tailwind utilities;
   ```

5. `src/App.jsx` ํŒŒ์ผ์„ ์•„๋ž˜์™€ ๊ฐ™์ด ์ˆ˜์ •ํ•˜์—ฌ ๊ธฐ๋ณธ ๋ ˆ์ด์•„์›ƒ๊ณผ ์˜ํ™” ๋ชฉ๋ก์„ ํ‘œ์‹œ:

   ```jsx
   import { useState, useEffect } from 'react'
   import axios from 'axios'
   import './App.css'

   const API_URL = 'http://localhost:8000/api/movies'

   function App() {
     const [movies, setMovies] = useState([])
     const [loading, setLoading] = useState(true)

     useEffect(() => {
       fetchMovies()
     }, [])

     const fetchMovies = async () => {
       try {
         const response = await axios.get(API_URL)
         setMovies(response.data)
       } catch (error) {
         console.error('Error fetching movies:', error)
       } finally {
         setLoading(false)
       }
     }

     const renderStars = (rating) => {
       return 'โ˜…'.repeat(rating) + 'โ˜†'.repeat(5 - rating)
     }

     if (loading) {
       return <div className="flex justify-center items-center h-screen text-xl">Loading...</div>
     }

     return (
       <div className="min-h-screen bg-gray-100 py-8">
         <div className="max-w-6xl mx-auto px-4">
           <header className="text-center mb-8">
             <h1 className="text-4xl font-bold text-gray-800 mb-2">๐ŸŽฌ ์˜ํ™” ๊ฐ์ƒ ํ๋ ˆ์ด์…˜</h1>
             <p className="text-gray-600">๋ณธ ์˜ํ™” ํ‰์  & ์ถ”์ฒœ ๋ฆฌ์ŠคํŠธ</p>
           </header>

           <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
             {movies.length === 0 ? (
               <div className="col-span-full text-center text-gray-500 py-12">
                 ์•„์ง ๋“ฑ๋ก๋œ ์˜ํ™”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ์ฒซ ์˜ํ™”๋ฅผ ์ถ”๊ฐ€ํ•ด๋ณด์„ธ์š”!
               </div>
             ) : (
               movies.map((movie) => (
                 <div key={movie.id} className="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-xl transition-shadow">
                   {movie.poster_url && (
                     <img src={movie.poster_url} alt={movie.title} className="w-full h-64 object-cover" />
                   )}
                   <div className="p-4">
                     <h3 className="text-xl font-semibold text-gray-800 mb-2">{movie.title}</h3>
                     <p className="text-sm text-gray-500 mb-2">์žฅ๋ฅด: {movie.genre}</p>
                     <div className="text-yellow-500 text-2xl mb-2">{renderStars(movie.rating)}</div>
                     {movie.comment && (
                       <p className="text-gray-700 text-sm italic">"{movie.comment}"</p>
                     )}
                     {movie.is_wishlist && (
                       <span className="inline-block mt-2 px-2 py-1 bg-purple-100 text-purple-700 text-xs rounded">์œ„์‹œ๋ฆฌ์ŠคํŠธ</span>
                     )}
                   </div>
                 </div>
               ))
             )}
           </div>
         </div>
       </div>
     )
   }

   export default App
   ```

6. React ๊ฐœ๋ฐœ ์„œ๋ฒ„ ์‹คํ–‰:
   ```bash
   npm run dev
   ```
   (๊ธฐ๋ณธ ํฌํŠธ: http://localhost:5173)

๋ธŒ๋ผ์šฐ์ €์—์„œ http://localhost:5173์„ ์—ด๋ฉด ์˜ํ™” ๋ชฉ๋ก์ด ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค (ํ˜„์žฌ๋Š” ๋น„์–ด ์žˆ์Œ).

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: React ํ”„๋ก ํŠธ์—”๋“œ๊ฐ€ http://localhost:5173์—์„œ ์‹คํ–‰๋˜๋ฉฐ, Tailwind CSS๊ฐ€ ์ ์šฉ๋œ ์˜ํ™” ๋ชฉ๋ก ํŽ˜์ด์ง€๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. Laravel API์—์„œ ์˜ํ™” ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์™€ ์นด๋“œ ํ˜•ํƒœ๋กœ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค. ์•„์ง ์˜ํ™”๊ฐ€ ์—†์œผ๋ฏ€๋กœ ๋นˆ ์ƒํƒœ ๋ฉ”์‹œ์ง€๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.

3
Step 3
**์˜ํ™” ์ถ”๊ฐ€ ํผ ๊ตฌํ˜„:**

์‚ฌ์šฉ์ž๊ฐ€ ์ƒˆ๋กœ์šด ์˜ํ™”๋ฅผ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๋Š” ํผ์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

`src/App.jsx` ํŒŒ์ผ์„ ์ˆ˜์ •ํ•˜์—ฌ ์˜ํ™” ์ถ”๊ฐ€ ํผ๊ณผ POST ์š”์ฒญ ๋กœ์ง์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค:

```jsx
import { useState, useEffect } from 'react'
import axios from 'axios'
import './App.css'

const API_URL = 'http://localhost:8000/api/movies'

function App() {
  const [movies, setMovies] = useState([])
  const [loading, setLoading] = useState(true)
  const [showForm, setShowForm] = useState(false)
  const [formData, setFormData] = useState({
    title: '',
    poster_url: '',
    genre: '',
    rating: 3,
    comment: '',
    is_wishlist: false,
  })

  useEffect(() => {
    fetchMovies()
  }, [])

  const fetchMovies = async () => {
    try {
      const response = await axios.get(API_URL)
      setMovies(response.data)
    } catch (error) {
      console.error('Error fetching movies:', error)
    } finally {
      setLoading(false)
    }
  }

  const handleSubmit = async (e) => {
    e.preventDefault()
    try {
      await axios.post(API_URL, formData)
      setFormData({ title: '', poster_url: '', genre: '', rating: 3, comment: '', is_wishlist: false })
      setShowForm(false)
      fetchMovies()
    } catch (error) {
      console.error('Error adding movie:', error)
      alert('์˜ํ™” ์ถ”๊ฐ€์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.')
    }
  }

  const handleDelete = async (id) => {
    if (!confirm('์ด ์˜ํ™”๋ฅผ ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?')) return
    try {
      await axios.delete(`${API_URL}/${id}`)
      fetchMovies()
    } catch (error) {
      console.error('Error deleting movie:', error)
    }
  }

  const renderStars = (rating) => {
    return 'โ˜…'.repeat(rating) + 'โ˜†'.repeat(5 - rating)
  }

  if (loading) {
    return <div className="flex justify-center items-center h-screen text-xl">Loading...</div>
  }

  return (
    <div className="min-h-screen bg-gray-100 py-8">
      <div className="max-w-6xl mx-auto px-4">
        <header className="text-center mb-8">
          <h1 className="text-4xl font-bold text-gray-800 mb-2">๐ŸŽฌ ์˜ํ™” ๊ฐ์ƒ ํ๋ ˆ์ด์…˜</h1>
          <p className="text-gray-600">๋ณธ ์˜ํ™” ํ‰์  & ์ถ”์ฒœ ๋ฆฌ์ŠคํŠธ</p>
        </header>

        <div className="text-center mb-6">
          <button
            onClick={() => setShowForm(!showForm)}
            className="bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 px-6 rounded-lg shadow transition"
          >
            {showForm ? '์ทจ์†Œ' : '+ ์˜ํ™” ์ถ”๊ฐ€'}
          </button>
        </div>

        {showForm && (
          <div className="bg-white rounded-lg shadow-md p-6 mb-8 max-w-2xl mx-auto">
            <h2 className="text-2xl font-semibold mb-4">์ƒˆ ์˜ํ™” ์ถ”๊ฐ€</h2>
            <form onSubmit={handleSubmit} className="space-y-4">
              <div>
                <label className="block text-sm font-medium text-gray-700 mb-1">์˜ํ™” ์ œ๋ชฉ *</label>
                <input
                  type="text"
                  required
                  value={formData.title}
                  onChange={(e) => setFormData({ ...formData, title: e.target.value })}
                  className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
                />
              </div>
              <div>
                <label className="block text-sm font-medium text-gray-700 mb-1">ํฌ์Šคํ„ฐ URL</label>
                <input
                  type="url"
                  value={formData.poster_url}
                  onChange={(e) => setFormData({ ...formData, poster_url: e.target.value })}
                  className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
                  placeholder="https://example.com/poster.jpg"
                />
              </div>
              <div>
                <label className="block text-sm font-medium text-gray-700 mb-1">์žฅ๋ฅด *</label>
                <select
                  required
                  value={formData.genre}
                  onChange={(e) => setFormData({ ...formData, genre: e.target.value })}
                  className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
                >
                  <option value="">์„ ํƒํ•˜์„ธ์š”</option>
                  <option value="์•ก์…˜">์•ก์…˜</option>
                  <option value="์ฝ”๋ฏธ๋””">์ฝ”๋ฏธ๋””</option>
                  <option value="๋“œ๋ผ๋งˆ">๋“œ๋ผ๋งˆ</option>
                  <option value="SF">SF</option>
                  <option value="๋กœ๋งจ์Šค">๋กœ๋งจ์Šค</option>
                  <option value="์Šค๋ฆด๋Ÿฌ">์Šค๋ฆด๋Ÿฌ</option>
                  <option value="์• ๋‹ˆ๋ฉ”์ด์…˜">์• ๋‹ˆ๋ฉ”์ด์…˜</option>
                </select>
              </div>
              <div>
                <label className="block text-sm font-medium text-gray-700 mb-1">ํ‰์  *</label>
                <div className="flex items-center space-x-2">
                  <input
                    type="range"
                    min="1"
                    max="5"
                    value={formData.rating}
                    onChange={(e) => setFormData({ ...formData, rating: parseInt(e.target.value) })}
                    className="flex-1"
                  />
                  <span className="text-2xl text-yellow-500">{renderStars(formData.rating)}</span>
                </div>
              </div>
              <div>
                <label className="block text-sm font-medium text-gray-700 mb-1">ํ•œ์ค„ํ‰</label>
                <textarea
                  value={formData.comment}
                  onChange={(e) => setFormData({ ...formData, comment: e.target.value })}
                  className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
                  rows="3"
                  placeholder="์ด ์˜ํ™”์— ๋Œ€ํ•œ ์งง์€ ๊ฐ์ƒ์„ ๋‚จ๊ฒจ์ฃผ์„ธ์š”"
                />
              </div>
              <div className="flex items-center">
                <input
                  type="checkbox"
                  id="wishlist"
                  checked={formData.is_wishlist}
                  onChange={(e) => setFormData({ ...formData, is_wishlist: e.target.checked })}
                  className="mr-2"
                />
                <label htmlFor="wishlist" className="text-sm text-gray-700">์œ„์‹œ๋ฆฌ์ŠคํŠธ์— ์ถ”๊ฐ€ (์•„์ง ์•ˆ ๋ณธ ์˜ํ™”)</label>
              </div>
              <button
                type="submit"
                className="w-full bg-green-600 hover:bg-green-700 text-white font-semibold py-2 rounded-lg transition"
              >
                ์ถ”๊ฐ€ํ•˜๊ธฐ
              </button>
            </form>
          </div>
        )}

        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
          {movies.length === 0 ? (
            <div className="col-span-full text-center text-gray-500 py-12">
              ์•„์ง ๋“ฑ๋ก๋œ ์˜ํ™”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ์ฒซ ์˜ํ™”๋ฅผ ์ถ”๊ฐ€ํ•ด๋ณด์„ธ์š”!
            </div>
          ) : (
            movies.map((movie) => (
              <div key={movie.id} className="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-xl transition-shadow">
                {movie.poster_url && (
                  <img src={movie.poster_url} alt={movie.title} className="w-full h-64 object-cover" />
                )}
                <div className="p-4">
                  <h3 className="text-xl font-semibold text-gray-800 mb-2">{movie.title}</h3>
                  <p className="text-sm text-gray-500 mb-2">์žฅ๋ฅด: {movie.genre}</p>
                  <div className="text-yellow-500 text-2xl mb-2">{renderStars(movie.rating)}</div>
                  {movie.comment && (
                    <p className="text-gray-700 text-sm italic mb-3">"{movie.comment}"</p>
                  )}
                  {movie.is_wishlist && (
                    <span className="inline-block mb-3 px-2 py-1 bg-purple-100 text-purple-700 text-xs rounded">์œ„์‹œ๋ฆฌ์ŠคํŠธ</span>
                  )}
                  <button
                    onClick={() => handleDelete(movie.id)}
                    className="w-full bg-red-500 hover:bg-red-600 text-white text-sm py-1 rounded transition"
                  >
                    ์‚ญ์ œ
                  </button>
                </div>
              </div>
            ))
          )}
        </div>
      </div>
    </div>
  )
}

export default App
```

์ด์ œ "+ ์˜ํ™” ์ถ”๊ฐ€" ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด ํผ์ด ๋‚˜ํƒ€๋‚˜๊ณ , ์˜ํ™” ์ •๋ณด๋ฅผ ์ž…๋ ฅํ•˜์—ฌ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ์˜ํ™” ์ถ”๊ฐ€ ํผ์ด ํ‘œ์‹œ๋˜๋ฉฐ, ์ œ๋ชฉ/ํฌ์Šคํ„ฐURL/์žฅ๋ฅด/ํ‰์ /ํ•œ์ค„ํ‰/์œ„์‹œ๋ฆฌ์ŠคํŠธ ์—ฌ๋ถ€๋ฅผ ์ž…๋ ฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํผ ์ œ์ถœ ์‹œ Laravel API๋กœ POST ์š”์ฒญ์ด ์ „์†ก๋˜๊ณ , ์„ฑ๊ณตํ•˜๋ฉด ์˜ํ™” ๋ชฉ๋ก์ด ์ƒˆ๋กœ๊ณ ์นจ๋˜์–ด ์ถ”๊ฐ€๋œ ์˜ํ™”๊ฐ€ ์นด๋“œ๋กœ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. ๊ฐ ์˜ํ™” ์นด๋“œ์— ์‚ญ์ œ ๋ฒ„ํŠผ์ด ์žˆ์–ด ํด๋ฆญ ์‹œ ํ•ด๋‹น ์˜ํ™”๊ฐ€ ์‚ญ์ œ๋ฉ๋‹ˆ๋‹ค.

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

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

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

๋Œ“๊ธ€ (0)

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