๐ŸŽ“ ์˜จ๋ผ์ธ ๊ฐ•์ขŒ ์ˆ˜๊ฐ•์ƒ ์ˆ˜๋ฃŒ์ฆ ์ž๋™ ๋ฐœ๊ธ‰ ์‹œ์Šคํ…œ - ๋ฐ”์ด๋ธŒ์ฝ”๋”ฉ ๋ ˆ์‹œํ”ผ
์ค‘๊ธ‰ PHP + JavaScript Laravel + React + Inertia.js AI: Claude

๐ŸŽ“ ์˜จ๋ผ์ธ ๊ฐ•์ขŒ ์ˆ˜๊ฐ•์ƒ ์ˆ˜๋ฃŒ์ฆ ์ž๋™ ๋ฐœ๊ธ‰ ์‹œ์Šคํ…œ

๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋ช… course-completion-certificate-automation
์กฐํšŒ์ˆ˜ 139 0๊ฑด ์ œ์ถœ

๋ฌธ์ œ ์„ค๋ช…

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

์˜จ๋ผ์ธ ๊ต์œก ํ”Œ๋žซํผ์—์„œ ์ˆ˜๊ฐ•์ƒ์ด ๊ฐ•์ขŒ๋ฅผ ์™„๋ฃŒํ•˜๋ฉด ์ž๋™์œผ๋กœ ์ˆ˜๋ฃŒ์ฆ์„ ์ƒ์„ฑํ•˜๊ณ  ๋ฐœ๊ธ‰ํ•˜๋Š” ํ’€์Šคํƒ ์‹œ์Šคํ…œ์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค. Laravel 13.x + Inertia.js + React 19 ๋ชจ๋…ธ๋ฆฌ์Šค ์•„ํ‚คํ…์ฒ˜๋กœ ๊ตฌํ˜„ํ•˜๋ฉฐ, ๊ด€๋ฆฌ์ž๊ฐ€ ์•„๋‹Œ ์‹ค์ œ ์ˆ˜๊ฐ•์ƒ์ด ์‚ฌ์šฉํ•˜๋Š” ์„œ๋น„์Šค์ž…๋‹ˆ๋‹ค.

โœจ ํ•ต์‹ฌ ๊ธฐ๋Šฅ

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

  1. ๊ฐ•์ขŒ ๋ชฉ๋ก ํŽ˜์ด์ง€: ์ˆ˜๊ฐ• ๊ฐ€๋Šฅํ•œ ๊ฐ•์ขŒ ๋ชฉ๋ก์„ ์นด๋“œ ํ˜•ํƒœ๋กœ ํ‘œ์‹œ (์ธ๋„ค์ผ, ์ œ๋ชฉ, ๊ฐ•์‚ฌ๋ช…, ์ˆ˜๊ฐ• ์‹œ๊ฐ„)
  2. ๊ฐ•์ขŒ ์ƒ์„ธ ํŽ˜์ด์ง€: ์ปค๋ฆฌํ˜๋Ÿผ, ๊ฐ•์‚ฌ ์†Œ๊ฐœ, ์ˆ˜๊ฐ• ์‹œ์ž‘ ๋ฒ„ํŠผ
  3. ํ•™์Šต ์ง„ํ–‰ ํŠธ๋ž˜์ปค: ๊ฐ ๊ฐ•์˜ ์˜์ƒ์„ ์ฒดํฌ๋ฆฌ์ŠคํŠธ๋กœ ํ‘œ์‹œํ•˜๊ณ , ์‹œ์ฒญ ์™„๋ฃŒ ์‹œ ์ฒดํฌ ํ‘œ์‹œ
  4. ์ง„ํ–‰๋ฅ  ์‹œ๊ฐํ™”: ํ”„๋กœ๊ทธ๋ ˆ์Šค ๋ฐ”๋กœ ์ „์ฒด ๊ฐ•์ขŒ ์ง„ํ–‰๋ฅ  ํ‘œ์‹œ (์˜ˆ: 75% ์™„๋ฃŒ)
  5. ์ˆ˜๋ฃŒ์ฆ ์ž๋™ ์ƒ์„ฑ: 100% ์™„๋ฃŒ ์‹œ ์ˆ˜๊ฐ•์ƒ ์ด๋ฆ„, ๊ฐ•์ขŒ๋ช…, ์™„๋ฃŒ ๋‚ ์งœ๊ฐ€ ํฌํ•จ๋œ ์ˆ˜๋ฃŒ์ฆ HTML ํŽ˜์ด์ง€ ์ƒ์„ฑ
  6. ์ˆ˜๋ฃŒ์ฆ ๋‹ค์šด๋กœ๋“œ: ์ƒ์„ฑ๋œ ์ˆ˜๋ฃŒ์ฆ์„ PDF๋กœ ๋‹ค์šด๋กœ๋“œ (DomPDF ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํ™œ์šฉ)
  7. ์ˆ˜๋ฃŒ ์ด๋ ฅ: ๋‚ด๊ฐ€ ์ˆ˜๋ฃŒํ•œ ๊ฐ•์ขŒ ๋ชฉ๋ก๊ณผ ์ˆ˜๋ฃŒ์ฆ ์žฌ๋‹ค์šด๋กœ๋“œ ๊ธฐ๋Šฅ

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

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

๐ŸŽฏ ํ•™์Šต ๋ชฉํ‘œ

  • Laravel Eloquent ๊ด€๊ณ„ ์„ค์ • (Course, Lesson, Enrollment, Certificate)
  • Inertia.js๋กœ SPA ๋ผ์šฐํŒ… ๊ตฌํ˜„ (ํŽ˜์ด์ง€ ์ „ํ™˜ ์‹œ ๊นœ๋นก์ž„ ์—†์Œ)
  • React ์ปดํฌ๋„ŒํŠธ ์ƒํƒœ ๊ด€๋ฆฌ (์ง„ํ–‰๋ฅ  ๊ณ„์‚ฐ, ์ฒดํฌ๋ฐ•์Šค ์ƒํƒœ)
  • PDF ์ƒ์„ฑ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํ†ตํ•ฉ (DomPDF)
  • ์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง (์ˆ˜๋ฃŒ ์ „/ํ›„ UI ๋ณ€ํ™”)

๐Ÿ› ๏ธ ๊ธฐ์ˆ  ์Šคํƒ

  • ๋ฐฑ์—”๋“œ: Laravel 13.x (PHP 8.3)
  • ํ”„๋ก ํŠธ์—”๋“œ: React 19 + Inertia.js
  • ์Šคํƒ€์ผ๋ง: Tailwind CSS
  • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค: SQLite (๊ฐœ๋ฐœ์šฉ) / MySQL (ํ”„๋กœ๋•์…˜ ๊ฐ€๋Šฅ)
  • PDF ์ƒ์„ฑ: barryvdh/laravel-dompdf
  • ์ธ์ฆ: Laravel Breeze (React + Inertia starter kit)

๐Ÿ“ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ตฌ์กฐ

  • courses: id, title, description, instructor, thumbnail, duration
  • lessons: id, course_id, title, video_url, order
  • enrollments: id, user_id, course_id, created_at
  • lesson_completions: id, enrollment_id, lesson_id, completed_at
  • certificates: id, enrollment_id, issued_at, certificate_code

๐ŸŽจ UI/UX ํฌ์ธํŠธ

  • ๊ฐ•์ขŒ ์นด๋“œ์— ํ˜ธ๋ฒ„ ์‹œ ๊ทธ๋ฆผ์ž ํšจ๊ณผ + ํ™•๋Œ€ ์• ๋‹ˆ๋ฉ”์ด์…˜
  • ์ง„ํ–‰๋ฅ  ํ”„๋กœ๊ทธ๋ ˆ์Šค ๋ฐ” ์• ๋‹ˆ๋ฉ”์ด์…˜ (0%์—์„œ ํ˜„์žฌ ์ง„ํ–‰๋ฅ ๊นŒ์ง€ ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ์ฆ๊ฐ€)
  • ์ˆ˜๋ฃŒ์ฆ ์ƒ์„ฑ ์‹œ ์ถ•ํ•˜ ๋ชจ๋‹ฌ ํŒ์—… + ์ปจํŽ˜ํ‹ฐ(์ƒ‰์ข…์ด) ์• ๋‹ˆ๋ฉ”์ด์…˜
  • ์ˆ˜๋ฃŒ์ฆ ๋ฏธ๋ฆฌ๋ณด๊ธฐ (๋‹ค์šด๋กœ๋“œ ์ „ ๋ธŒ๋ผ์šฐ์ €์—์„œ ํ™•์ธ)
  • ๋ฐ˜์‘ํ˜• ๋””์ž์ธ (๋ชจ๋ฐ”์ผ์—์„œ๋„ ํŽธํ•˜๊ฒŒ ํ•™์Šต)

์ œ์•ฝ ์กฐ๊ฑด

  • โ€ข Laravel 13.x + Inertia.js + React 19 ๋ชจ๋…ธ๋ฆฌ์Šค ๊ตฌ์กฐ ์‚ฌ์šฉ (๋ณ„๋„ API ๋ผ์šฐํŠธ ์—†์Œ)
  • โ€ข npx create laravel ๋˜๋Š” Laravel Breeze (React + Inertia) starter kit์œผ๋กœ ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ
  • โ€ข Inertia::render()๋กœ Laravel ์ปจํŠธ๋กค๋Ÿฌ์—์„œ React ์ปดํฌ๋„ŒํŠธ์— ๋ฐ์ดํ„ฐ ์ „๋‹ฌ
  • โ€ข SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์‚ฌ์šฉ (๊ฐ„ํŽธํ•œ ๋กœ์ปฌ ๊ฐœ๋ฐœ)
  • โ€ข Tailwind CSS๋กœ ์Šคํƒ€์ผ๋ง (์œ ํ‹ธ๋ฆฌํ‹ฐ ํด๋ž˜์Šค ๊ธฐ๋ฐ˜)
  • โ€ข barryvdh/laravel-dompdf ํŒจํ‚ค์ง€๋กœ PDF ์ƒ์„ฑ
  • โ€ข ์‚ฌ์šฉ์ž ์ธ์ฆ ๊ตฌํ˜„ (Laravel Breeze ๊ธฐ๋ณธ ์ œ๊ณต)
  • โ€ข Eloquent ๊ด€๊ณ„ (Course hasMany Lessons, User hasMany Enrollments ๋“ฑ) ์ •์˜
  • โ€ข React ํ•จ์ˆ˜ํ˜• ์ปดํฌ๋„ŒํŠธ + Hooks (useState, useEffect) ์‚ฌ์šฉ
  • โ€ข ์‹ค์‹œ๊ฐ„ ์ฑ„ํŒ… ๊ธฐ๋Šฅ ๊ธˆ์ง€ (HTTP ๊ธฐ๋ฐ˜ ์š”์ฒญ๋งŒ ์‚ฌ์šฉ)

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

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

1
Step 1
mkdir course-completion-certificate-automation && cd course-completion-certificate-automation

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

๋‹ค์Œ ๋ช…๋ น์–ด๋กœ Laravel Breeze (React + Inertia) starter kit์„ ์„ค์น˜ํ•˜์„ธ์š”:

```bash
composer create-project laravel/laravel .
composer require laravel/breeze --dev
php artisan breeze:install react
php artisan migrate
npm install
npm run dev
```

์„ค์น˜ ํ›„ ๋‹ค์Œ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜์„ธ์š”:

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

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

3. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ 4๊ฐœ ์ƒ์„ฑ:
   - `create_courses_table`: id, title, description, instructor, thumbnail, duration(๋ถ„), timestamps
   - `create_lessons_table`: id, course_id, title, video_url, order, timestamps
   - `create_enrollments_table`: id, user_id, course_id, timestamps
   - `create_lesson_completions_table`: id, enrollment_id, lesson_id, completed_at, timestamps
   - `create_certificates_table`: id, enrollment_id, issued_at, certificate_code(์œ ๋‹ˆํฌ), timestamps

4. ๊ฐ ํ…Œ์ด๋ธ”์˜ ์™ธ๋ž˜ ํ‚ค ์ œ์•ฝ ์กฐ๊ฑด ์ถ”๊ฐ€:
   - lessons.course_id โ†’ courses.id (onDelete cascade)
   - enrollments.user_id โ†’ users.id (onDelete cascade)
   - enrollments.course_id โ†’ courses.id (onDelete cascade)
   - lesson_completions.enrollment_id โ†’ enrollments.id (onDelete cascade)
   - lesson_completions.lesson_id โ†’ lessons.id (onDelete cascade)
   - certificates.enrollment_id โ†’ enrollments.id (onDelete cascade)

5. `php artisan migrate` ์‹คํ–‰ํ•˜์—ฌ ํ…Œ์ด๋ธ” ์ƒ์„ฑ ํ™•์ธ

์™„๋ฃŒ๋˜๋ฉด "Laravel Breeze + Inertia + React ์„ค์น˜ ์™„๋ฃŒ, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์„ฑ๊ณต"์ด๋ผ๊ณ  ์•Œ๋ ค์ฃผ์„ธ์š”.

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: Laravel 13.x ํ”„๋กœ์ ํŠธ๊ฐ€ ์ƒ์„ฑ๋˜๊ณ , Breeze + Inertia + React๊ฐ€ ์„ค์น˜๋ฉ๋‹ˆ๋‹ค. SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๊ฐ€ ์„ค์ •๋˜๊ณ , courses, lessons, enrollments, lesson_completions, certificates ํ…Œ์ด๋ธ”์ด ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค. ๊ธฐ๋ณธ ์ธ์ฆ ๊ธฐ๋Šฅ(๋กœ๊ทธ์ธ/ํšŒ์›๊ฐ€์ž…)์ด ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค.

2
Step 2
์ด์ œ Eloquent ๋ชจ๋ธ๊ณผ ๊ด€๊ณ„๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

1. ๋‹ค์Œ ๋ชจ๋ธ ํŒŒ์ผ ์ƒ์„ฑ:
   ```bash
   php artisan make:model Course
   php artisan make:model Lesson
   php artisan make:model Enrollment
   php artisan make:model LessonCompletion
   php artisan make:model Certificate
   ```

2. `app/Models/Course.php`์— ๊ด€๊ณ„ ์ •์˜:
   ```php
   public function lessons()
   {
       return $this->hasMany(Lesson::class)->orderBy('order');
   }

   public function enrollments()
   {
       return $this->hasMany(Enrollment::class);
   }
   ```

3. `app/Models/Lesson.php`์— ๊ด€๊ณ„ ์ •์˜:
   ```php
   public function course()
   {
       return $this->belongsTo(Course::class);
   }
   ```

4. `app/Models/Enrollment.php`์— ๊ด€๊ณ„ ์ •์˜:
   ```php
   public function user()
   {
       return $this->belongsTo(User::class);
   }

   public function course()
   {
       return $this->belongsTo(Course::class);
   }

   public function lessonCompletions()
   {
       return $this->hasMany(LessonCompletion::class);
   }

   public function certificate()
   {
       return $this->hasOne(Certificate::class);
   }

   // ์ง„ํ–‰๋ฅ  ๊ณ„์‚ฐ ๋ฉ”์„œ๋“œ
   public function getProgressPercentageAttribute()
   {
       $totalLessons = $this->course->lessons()->count();
       if ($totalLessons === 0) return 0;
       $completedLessons = $this->lessonCompletions()->count();
       return round(($completedLessons / $totalLessons) * 100);
   }

   public function isCompleted()
   {
       return $this->progress_percentage === 100;
   }
   ```

5. `app/Models/LessonCompletion.php`์— ๊ด€๊ณ„ ์ •์˜:
   ```php
   protected $fillable = ['enrollment_id', 'lesson_id', 'completed_at'];

   public function enrollment()
   {
       return $this->belongsTo(Enrollment::class);
   }

   public function lesson()
   {
       return $this->belongsTo(Lesson::class);
   }
   ```

6. `app/Models/Certificate.php`์— ๊ด€๊ณ„ ์ •์˜:
   ```php
   protected $fillable = ['enrollment_id', 'issued_at', 'certificate_code'];

   public function enrollment()
   {
       return $this->belongsTo(Enrollment::class);
   }
   ```

7. Seeder ์ƒ์„ฑํ•˜์—ฌ ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€:
   ```bash
   php artisan make:seeder CourseSeeder
   ```

   `database/seeders/CourseSeeder.php`์— ๋‹ค์Œ ๋‚ด์šฉ ์ž‘์„ฑ:
   ```php
   use App\Models\Course;
   use App\Models\Lesson;

   public function run()
   {
       $course1 = Course::create([
           'title' => 'Laravel ๋งˆ์Šคํ„ฐ ํด๋ž˜์Šค',
           'description' => 'Laravel 13.x๋กœ ํ’€์Šคํƒ ์›น ๊ฐœ๋ฐœ ์™„์ „ ์ •๋ณต',
           'instructor' => '๊น€๊ฐœ๋ฐœ',
           'thumbnail' => 'https://via.placeholder.com/400x250?text=Laravel+Course',
           'duration' => 180,
       ]);

       Lesson::create(['course_id' => $course1->id, 'title' => '1๊ฐ•: Laravel ์†Œ๊ฐœ', 'video_url' => 'https://example.com/video1', 'order' => 1]);
       Lesson::create(['course_id' => $course1->id, 'title' => '2๊ฐ•: ๋ผ์šฐํŒ… ๊ธฐ์ดˆ', 'video_url' => 'https://example.com/video2', 'order' => 2]);
       Lesson::create(['course_id' => $course1->id, 'title' => '3๊ฐ•: Eloquent ORM', 'video_url' => 'https://example.com/video3', 'order' => 3]);
       Lesson::create(['course_id' => $course1->id, 'title' => '4๊ฐ•: Inertia.js ํ†ตํ•ฉ', 'video_url' => 'https://example.com/video4', 'order' => 4]);

       $course2 = Course::create([
           'title' => 'React 19 ์™„๋ฒฝ ๊ฐ€์ด๋“œ',
           'description' => '์ตœ์‹  React ๊ธฐ๋Šฅ๊ณผ Hooks ๋งˆ์Šคํ„ฐํ•˜๊ธฐ',
           'instructor' => '๋ฐ•๋ฆฌ์•กํŠธ',
           'thumbnail' => 'https://via.placeholder.com/400x250?text=React+Course',
           'duration' => 150,
       ]);

       Lesson::create(['course_id' => $course2->id, 'title' => '1๊ฐ•: React ๊ธฐ์ดˆ', 'video_url' => 'https://example.com/video5', 'order' => 1]);
       Lesson::create(['course_id' => $course2->id, 'title' => '2๊ฐ•: useState Hook', 'video_url' => 'https://example.com/video6', 'order' => 2]);
       Lesson::create(['course_id' => $course2->id, 'title' => '3๊ฐ•: useEffect Hook', 'video_url' => 'https://example.com/video7', 'order' => 3]);
   }
   ```

8. DatabaseSeeder์—์„œ CourseSeeder ํ˜ธ์ถœ:
   ```php
   public function run()
   {
       $this->call(CourseSeeder::class);
   }
   ```

9. ์‹œ๋” ์‹คํ–‰:
   ```bash
   php artisan db:seed
   ```

์™„๋ฃŒ๋˜๋ฉด "๋ชจ๋ธ ๊ด€๊ณ„ ์„ค์ • ์™„๋ฃŒ, ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€๋จ"์ด๋ผ๊ณ  ์•Œ๋ ค์ฃผ์„ธ์š”.

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: Course, Lesson, Enrollment, LessonCompletion, Certificate ๋ชจ๋ธ์ด ์ƒ์„ฑ๋˜๊ณ , Eloquent ๊ด€๊ณ„๊ฐ€ ์ •์˜๋ฉ๋‹ˆ๋‹ค. CourseSeeder๋กœ ์ƒ˜ํ”Œ ๊ฐ•์ขŒ 2๊ฐœ์™€ ๊ฐ•์˜ ์˜์ƒ 7๊ฐœ๊ฐ€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ถ”๊ฐ€๋ฉ๋‹ˆ๋‹ค. ์ง„ํ–‰๋ฅ  ๊ณ„์‚ฐ ๋ฉ”์„œ๋“œ๊ฐ€ Enrollment ๋ชจ๋ธ์— ๊ตฌํ˜„๋ฉ๋‹ˆ๋‹ค.

3
Step 3
์ด์ œ ๊ฐ•์ขŒ ๋ชฉ๋ก ํŽ˜์ด์ง€์™€ ์ƒ์„ธ ํŽ˜์ด์ง€๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

1. CourseController ์ƒ์„ฑ:
   ```bash
   php artisan make:controller CourseController
   ```

2. `app/Http/Controllers/CourseController.php`์— ๋‹ค์Œ ๋ฉ”์„œ๋“œ ์ž‘์„ฑ:
   ```php
   use App\Models\Course;
   use App\Models\Enrollment;
   use Inertia\Inertia;

   public function index()
   {
       $courses = Course::withCount('lessons')->get();
       $enrolledCourseIds = auth()->user()->enrollments()->pluck('course_id')->toArray();

       return Inertia::render('Courses/Index', [
           'courses' => $courses,
           'enrolledCourseIds' => $enrolledCourseIds,
       ]);
   }

   public function show(Course $course)
   {
       $course->load('lessons');
       $enrollment = auth()->user()->enrollments()->where('course_id', $course->id)->first();

       $completedLessonIds = [];
       $progress = 0;

       if ($enrollment) {
           $completedLessonIds = $enrollment->lessonCompletions()->pluck('lesson_id')->toArray();
           $progress = $enrollment->progress_percentage;
       }

       return Inertia::render('Courses/Show', [
           'course' => $course,
           'enrollment' => $enrollment,
           'completedLessonIds' => $completedLessonIds,
           'progress' => $progress,
       ]);
   }

   public function enroll(Course $course)
   {
       $enrollment = auth()->user()->enrollments()->firstOrCreate([
           'course_id' => $course->id,
       ]);

       return redirect()->route('courses.show', $course);
   }
   ```

3. `routes/web.php`์— ๋ผ์šฐํŠธ ์ถ”๊ฐ€:
   ```php
   use App\Http\Controllers\CourseController;

   Route::middleware('auth')->group(function () {
       Route::get('/courses', [CourseController::class, 'index'])->name('courses.index');
       Route::get('/courses/{course}', [CourseController::class, 'show'])->name('courses.show');
       Route::post('/courses/{course}/enroll', [CourseController::class, 'enroll'])->name('courses.enroll');
   });
   ```

4. React ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ: `resources/js/Pages/Courses/Index.jsx`
   ```jsx
   import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
   import { Head, Link } from '@inertiajs/react';

   export default function Index({ auth, courses, enrolledCourseIds }) {
       return (
           <AuthenticatedLayout user={auth.user}>
               <Head title="๊ฐ•์ขŒ ๋ชฉ๋ก" />

               <div className="py-12">
                   <div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
                       <h1 className="text-3xl font-bold text-gray-900 mb-8">์ˆ˜๊ฐ• ๊ฐ€๋Šฅํ•œ ๊ฐ•์ขŒ</h1>

                       <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
                           {courses.map((course) => (
                               <Link
                                   key={course.id}
                                   href={route('courses.show', course.id)}
                                   className="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-xl transition-shadow duration-300 transform hover:scale-105"
                               >
                                   <img
                                       src={course.thumbnail}
                                       alt={course.title}
                                       className="w-full h-48 object-cover"
                                   />
                                   <div className="p-6">
                                       <h2 className="text-xl font-semibold text-gray-900 mb-2">
                                           {course.title}
                                       </h2>
                                       <p className="text-gray-600 text-sm mb-4">
                                           ๊ฐ•์‚ฌ: {course.instructor}
                                       </p>
                                       <div className="flex justify-between items-center">
                                           <span className="text-gray-500 text-sm">
                                               {course.lessons_count}๊ฐœ ๊ฐ•์˜
                                           </span>
                                           <span className="text-gray-500 text-sm">
                                               {course.duration}๋ถ„
                                           </span>
                                       </div>
                                       {enrolledCourseIds.includes(course.id) && (
                                           <div className="mt-4">
                                               <span className="inline-block bg-green-100 text-green-800 text-xs px-3 py-1 rounded-full">
                                                   ์ˆ˜๊ฐ• ์ค‘
                                               </span>
                                           </div>
                                       )}
                                   </div>
                               </Link>
                           ))}
                       </div>
                   </div>
               </div>
           </AuthenticatedLayout>
       );
   }
   ```

5. React ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ: `resources/js/Pages/Courses/Show.jsx`
   ```jsx
   import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
   import { Head, router } from '@inertiajs/react';

   export default function Show({ auth, course, enrollment, completedLessonIds, progress }) {
       const handleEnroll = () => {
           router.post(route('courses.enroll', course.id));
       };

       return (
           <AuthenticatedLayout user={auth.user}>
               <Head title={course.title} />

               <div className="py-12">
                   <div className="max-w-4xl mx-auto sm:px-6 lg:px-8">
                       <div className="bg-white rounded-lg shadow-md overflow-hidden">
                           <img
                               src={course.thumbnail}
                               alt={course.title}
                               className="w-full h-64 object-cover"
                           />
                           <div className="p-8">
                               <h1 className="text-3xl font-bold text-gray-900 mb-4">
                                   {course.title}
                               </h1>
                               <p className="text-gray-700 mb-6">{course.description}</p>
                               <div className="flex items-center justify-between mb-8">
                                   <div>
                                       <p className="text-gray-600">๊ฐ•์‚ฌ: {course.instructor}</p>
                                       <p className="text-gray-600">
                                           ์ด {course.lessons.length}๊ฐœ ๊ฐ•์˜ โ€ข {course.duration}๋ถ„
                                       </p>
                                   </div>
                                   {!enrollment && (
                                       <button
                                           onClick={handleEnroll}
                                           className="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 transition-colors"
                                       >
                                           ์ˆ˜๊ฐ• ์‹œ์ž‘ํ•˜๊ธฐ
                                       </button>
                                   )}
                               </div>

                               {enrollment && (
                                   <div className="mb-8">
                                       <div className="flex justify-between items-center mb-2">
                                           <span className="text-gray-700 font-medium">ํ•™์Šต ์ง„ํ–‰๋ฅ </span>
                                           <span className="text-blue-600 font-bold">{progress}%</span>
                                       </div>
                                       <div className="w-full bg-gray-200 rounded-full h-4">
                                           <div
                                               className="bg-blue-600 h-4 rounded-full transition-all duration-500"
                                               style={{ width: `${progress}%` }}
                                           ></div>
                                       </div>
                                   </div>
                               )}

                               <h2 className="text-2xl font-semibold text-gray-900 mb-4">์ปค๋ฆฌํ˜๋Ÿผ</h2>
                               <div className="space-y-3">
                                   {course.lessons.map((lesson, index) => (
                                       <div
                                           key={lesson.id}
                                           className={`p-4 rounded-lg border ${
                                               completedLessonIds.includes(lesson.id)
                                                   ? 'bg-green-50 border-green-300'
                                                   : 'bg-gray-50 border-gray-200'
                                           }`}
                                       >
                                           <div className="flex items-center justify-between">
                                               <div className="flex items-center space-x-3">
                                                   {completedLessonIds.includes(lesson.id) ? (
                                                       <svg className="w-6 h-6 text-green-600" fill="currentColor" viewBox="0 0 20 20">
                                                           <path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
                                                       </svg>
                                                   ) : (
                                                       <div className="w-6 h-6 rounded-full border-2 border-gray-400"></div>
                                                   )}
                                                   <span className="font-medium text-gray-900">{lesson.title}</span>
                                               </div>
                                           </div>
                                       </div>
                                   ))}
                               </div>
                           </div>
                       </div>
                   </div>
               </div>
           </AuthenticatedLayout>
       );
   }
   ```

6. ๋„ค๋น„๊ฒŒ์ด์…˜์— "๊ฐ•์ขŒ ๋ชฉ๋ก" ๋งํฌ ์ถ”๊ฐ€: `resources/js/Layouts/AuthenticatedLayout.jsx`์˜ NavLink ์„น์…˜์— ์ถ”๊ฐ€:
   ```jsx
   <NavLink href={route('courses.index')} active={route().current('courses.index')}>
       ๊ฐ•์ขŒ ๋ชฉ๋ก
   </NavLink>
   ```

์™„๋ฃŒ๋˜๋ฉด "๊ฐ•์ขŒ ๋ชฉ๋ก ๋ฐ ์ƒ์„ธ ํŽ˜์ด์ง€ ์™„์„ฑ"์ด๋ผ๊ณ  ์•Œ๋ ค์ฃผ์„ธ์š”.

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: CourseController๊ฐ€ ์ƒ์„ฑ๋˜๊ณ , ๊ฐ•์ขŒ ๋ชฉ๋ก ํŽ˜์ด์ง€์™€ ์ƒ์„ธ ํŽ˜์ด์ง€๊ฐ€ Inertia.js๋กœ ๋ Œ๋”๋ง๋ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๋Š” ๊ฐ•์ขŒ ์นด๋“œ๋ฅผ ํด๋ฆญํ•˜์—ฌ ์ƒ์„ธ ์ •๋ณด๋ฅผ ํ™•์ธํ•˜๊ณ , '์ˆ˜๊ฐ• ์‹œ์ž‘ํ•˜๊ธฐ' ๋ฒ„ํŠผ์œผ๋กœ ์ˆ˜๊ฐ• ๋“ฑ๋กํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋“ฑ๋ก ํ›„ ์ปค๋ฆฌํ˜๋Ÿผ์ด ์ฒดํฌ๋ฆฌ์ŠคํŠธ ํ˜•ํƒœ๋กœ ํ‘œ์‹œ๋˜๋ฉฐ, ์ง„ํ–‰๋ฅ  ํ”„๋กœ๊ทธ๋ ˆ์Šค ๋ฐ”๊ฐ€ ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค.

4
Step 4
์ด์ œ ๊ฐ•์˜ ์˜์ƒ ์‹œ์ฒญ ์™„๋ฃŒ ์ฒดํฌ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.

1. LessonController ์ƒ์„ฑ:
   ```bash
   php artisan make:controller LessonController
   ```

2. `app/Http/Controllers/LessonController.php`์— ๋ฉ”์„œ๋“œ ์ž‘์„ฑ:
   ```php
   use App\Models\Lesson;
   use App\Models\Enrollment;
   use App\Models\LessonCompletion;
   use Illuminate\Http\Request;

   public function toggleCompletion(Request $request, Lesson $lesson)
   {
       $enrollment = auth()->user()->enrollments()
           ->where('course_id', $lesson->course_id)
           ->firstOrFail();

       $completion = LessonCompletion::where('enrollment_id', $enrollment->id)
           ->where('lesson_id', $lesson->id)
           ->first();

       if ($completion) {
           // ์ด๋ฏธ ์™„๋ฃŒ๋œ ๊ฒฝ์šฐ ์‚ญ์ œ (์ฒดํฌ ํ•ด์ œ)
           $completion->delete();
           $message = '์™„๋ฃŒ ํ‘œ์‹œ๊ฐ€ ํ•ด์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.';
       } else {
           // ์™„๋ฃŒ ํ‘œ์‹œ
           LessonCompletion::create([
               'enrollment_id' => $enrollment->id,
               'lesson_id' => $lesson->id,
               'completed_at' => now(),
           ]);
           $message = '๊ฐ•์˜ ์‹œ์ฒญ์„ ์™„๋ฃŒํ–ˆ์Šต๋‹ˆ๋‹ค!';
       }

       // ์ง„ํ–‰๋ฅ  ์žฌ๊ณ„์‚ฐ
       $progress = $enrollment->fresh()->progress_percentage;

       return back()->with([
           'message' => $message,
           'progress' => $progress,
       ]);
   }
   ```

3. `routes/web.php`์— ๋ผ์šฐํŠธ ์ถ”๊ฐ€:
   ```php
   use App\Http\Controllers\LessonController;

   Route::middleware('auth')->group(function () {
       Route::post('/lessons/{lesson}/toggle-completion', [LessonController::class, 'toggleCompletion'])->name('lessons.toggle-completion');
   });
   ```

4. `resources/js/Pages/Courses/Show.jsx` ์ˆ˜์ • (๊ฐ•์˜ ํ•ญ๋ชฉ์— ์ฒดํฌ๋ฐ•์Šค ์ถ”๊ฐ€):
   ```jsx
   import { Head, router, usePage } from '@inertiajs/react';
   import { useEffect } from 'react';

   export default function Show({ auth, course, enrollment, completedLessonIds, progress }) {
       const { flash } = usePage().props;

       useEffect(() => {
           if (flash.message) {
               alert(flash.message);
           }
       }, [flash]);

       const handleEnroll = () => {
           router.post(route('courses.enroll', course.id));
       };

       const handleToggleCompletion = (lessonId) => {
           router.post(route('lessons.toggle-completion', lessonId));
       };

       return (
           <AuthenticatedLayout user={auth.user}>
               <Head title={course.title} />

               <div className="py-12">
                   <div className="max-w-4xl mx-auto sm:px-6 lg:px-8">
                       <div className="bg-white rounded-lg shadow-md overflow-hidden">
                           <img
                               src={course.thumbnail}
                               alt={course.title}
                               className="w-full h-64 object-cover"
                           />
                           <div className="p-8">
                               <h1 className="text-3xl font-bold text-gray-900 mb-4">
                                   {course.title}
                               </h1>
                               <p className="text-gray-700 mb-6">{course.description}</p>
                               <div className="flex items-center justify-between mb-8">
                                   <div>
                                       <p className="text-gray-600">๊ฐ•์‚ฌ: {course.instructor}</p>
                                       <p className="text-gray-600">
                                           ์ด {course.lessons.length}๊ฐœ ๊ฐ•์˜ โ€ข {course.duration}๋ถ„
                                       </p>
                                   </div>
                                   {!enrollment && (
                                       <button
                                           onClick={handleEnroll}
                                           className="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 transition-colors"
                                       >
                                           ์ˆ˜๊ฐ• ์‹œ์ž‘ํ•˜๊ธฐ
                                       </button>
                                   )}
                               </div>

                               {enrollment && (
                                   <div className="mb-8">
                                       <div className="flex justify-between items-center mb-2">
                                           <span className="text-gray-700 font-medium">ํ•™์Šต ์ง„ํ–‰๋ฅ </span>
                                           <span className="text-blue-600 font-bold">{progress}%</span>
                                       </div>
                                       <div className="w-full bg-gray-200 rounded-full h-4">
                                           <div
                                               className="bg-blue-600 h-4 rounded-full transition-all duration-500"
                                               style={{ width: `${progress}%` }}
                                           ></div>
                                       </div>
                                   </div>
                               )}

                               <h2 className="text-2xl font-semibold text-gray-900 mb-4">์ปค๋ฆฌํ˜๋Ÿผ</h2>
                               <div className="space-y-3">
                                   {course.lessons.map((lesson, index) => (
                                       <div
                                           key={lesson.id}
                                           className={`p-4 rounded-lg border ${
                                               completedLessonIds.includes(lesson.id)
                                                   ? 'bg-green-50 border-green-300'
                                                   : 'bg-gray-50 border-gray-200'
                                           }`}
                                       >
                                           <div className="flex items-center justify-between">
                                               <div className="flex items-center space-x-3">
                                                   <button
                                                       onClick={() => handleToggleCompletion(lesson.id)}
                                                       disabled={!enrollment}
                                                       className="focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed"
                                                   >
                                                       {completedLessonIds.includes(lesson.id) ? (
                                                           <svg className="w-6 h-6 text-green-600" fill="currentColor" viewBox="0 0 20 20">
                                                               <path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
                                                           </svg>
                                                       ) : (
                                                           <div className="w-6 h-6 rounded-full border-2 border-gray-400 hover:border-blue-500 transition-colors"></div>
                                                       )}
                                                   </button>
                                                   <span className="font-medium text-gray-900">{lesson.title}</span>
                                               </div>
                                               {enrollment && (
                                                   <a
                                                       href={lesson.video_url}
                                                       target="_blank"
                                                       rel="noopener noreferrer"
                                                       className="text-blue-600 hover:text-blue-800 text-sm font-medium"
                                                   >
                                                       ์˜์ƒ ๋ณด๊ธฐ โ†’
                                                   </a>
                                               )}
                                           </div>
                                       </div>
                                   ))}
                               </div>
                           </div>
                       </div>
                   </div>
               </div>
           </AuthenticatedLayout>
       );
   }
   ```

์™„๋ฃŒ๋˜๋ฉด "๊ฐ•์˜ ์‹œ์ฒญ ์™„๋ฃŒ ์ฒดํฌ ๊ธฐ๋Šฅ ๊ตฌํ˜„ ์™„๋ฃŒ"๋ผ๊ณ  ์•Œ๋ ค์ฃผ์„ธ์š”.

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: LessonController๊ฐ€ ์ƒ์„ฑ๋˜๊ณ , ๊ฐ•์˜ ํ•ญ๋ชฉ ์˜† ์ฒดํฌ๋ฐ•์Šค๋ฅผ ํด๋ฆญํ•˜๋ฉด ์™„๋ฃŒ ํ‘œ์‹œ๊ฐ€ ํ† ๊ธ€๋ฉ๋‹ˆ๋‹ค. ์™„๋ฃŒ๋œ ๊ฐ•์˜๋Š” ์ดˆ๋ก์ƒ‰ ๋ฐฐ๊ฒฝ์œผ๋กœ ๋ณ€๊ฒฝ๋˜๊ณ , ์ง„ํ–‰๋ฅ  ํ”„๋กœ๊ทธ๋ ˆ์Šค ๋ฐ”๊ฐ€ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์—…๋ฐ์ดํŠธ๋ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๋Š” '์˜์ƒ ๋ณด๊ธฐ' ๋งํฌ๋กœ ๊ฐ•์˜ ์˜์ƒ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

5
Step 5
์ด์ œ ์ˆ˜๋ฃŒ์ฆ ์ž๋™ ์ƒ์„ฑ ๋ฐ PDF ๋‹ค์šด๋กœ๋“œ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.

1. DomPDF ํŒจํ‚ค์ง€ ์„ค์น˜:
   ```bash
   composer require barryvdh/laravel-dompdf
   ```

2. CertificateController ์ƒ์„ฑ:
   ```bash
   php artisan make:controller CertificateController
   ```

3. `app/Http/Controllers/CertificateController.php`์— ๋ฉ”์„œ๋“œ ์ž‘์„ฑ:
   ```php
   use App\Models\Enrollment;
   use App\Models\Certificate;
   use Barryvdh\DomPDF\Facade\Pdf;
   use Illuminate\Support\Str;

   public function generate(Enrollment $enrollment)
   {
       // ๊ถŒํ•œ ํ™•์ธ
       if ($enrollment->user_id !== auth()->id()) {
           abort(403, '๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค.');
       }

       // 100% ์™„๋ฃŒ ํ™•์ธ
       if (!$enrollment->isCompleted()) {
           return back()->with('error', '๋ชจ๋“  ๊ฐ•์˜๋ฅผ ์™„๋ฃŒํ•ด์•ผ ์ˆ˜๋ฃŒ์ฆ์„ ๋ฐœ๊ธ‰๋ฐ›์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.');
       }

       // ์ด๋ฏธ ๋ฐœ๊ธ‰๋œ ์ˆ˜๋ฃŒ์ฆ์ด ์žˆ๋Š”์ง€ ํ™•์ธ
       $certificate = $enrollment->certificate;

       if (!$certificate) {
           // ์ˆ˜๋ฃŒ์ฆ ์ƒ์„ฑ
           $certificate = Certificate::create([
               'enrollment_id' => $enrollment->id,
               'issued_at' => now(),
               'certificate_code' => strtoupper(Str::random(12)),
           ]);
       }

       return redirect()->route('certificates.show', $certificate);
   }

   public function show(Certificate $certificate)
   {
       // ๊ถŒํ•œ ํ™•์ธ
       if ($certificate->enrollment->user_id !== auth()->id()) {
           abort(403, '๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค.');
       }

       $certificate->load('enrollment.user', 'enrollment.course');

       return Inertia::render('Certificates/Show', [
           'certificate' => $certificate,
       ]);
   }

   public function download(Certificate $certificate)
   {
       // ๊ถŒํ•œ ํ™•์ธ
       if ($certificate->enrollment->user_id !== auth()->id()) {
           abort(403, '๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค.');
       }

       $certificate->load('enrollment.user', 'enrollment.course');

       $pdf = Pdf::loadView('certificates.pdf', [
           'certificate' => $certificate,
           'userName' => $certificate->enrollment->user->name,
           'courseName' => $certificate->enrollment->course->title,
           'issuedDate' => $certificate->issued_at->format('Y๋…„ m์›” d์ผ'),
           'certificateCode' => $certificate->certificate_code,
       ]);

       return $pdf->download('์ˆ˜๋ฃŒ์ฆ_' . $certificate->enrollment->course->title . '.pdf');
   }
   ```

4. `routes/web.php`์— ๋ผ์šฐํŠธ ์ถ”๊ฐ€:
   ```php
   use App\Http\Controllers\CertificateController;

   Route::middleware('auth')->group(function () {
       Route::post('/enrollments/{enrollment}/generate-certificate', [CertificateController::class, 'generate'])->name('certificates.generate');
       Route::get('/certificates/{certificate}', [CertificateController::class, 'show'])->name('certificates.show');
       Route::get('/certificates/{certificate}/download', [CertificateController::class, 'download'])->name('certificates.download');
   });
   ```

5. PDF ํ…œํ”Œ๋ฆฟ ์ƒ์„ฑ: `resources/views/certificates/pdf.blade.php`
   ```blade
   <!DOCTYPE html>
   <html>
   <head>
       <meta charset="utf-8">
       <title>์ˆ˜๋ฃŒ์ฆ</title>
       <style>
           @page {
               margin: 0;
           }
           body {
               margin: 0;
               padding: 60px;
               font-family: 'DejaVu Sans', sans-serif;
               background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
               color: #333;
           }
           .certificate {
               background: white;
               padding: 80px 60px;
               border-radius: 20px;
               box-shadow: 0 20px 60px rgba(0,0,0,0.3);
               text-align: center;
           }
           .title {
               font-size: 48px;
               font-weight: bold;
               color: #667eea;
               margin-bottom: 40px;
               letter-spacing: 2px;
           }
           .subtitle {
               font-size: 24px;
               color: #666;
               margin-bottom: 60px;
           }
           .recipient {
               font-size: 36px;
               font-weight: bold;
               color: #333;
               margin-bottom: 20px;
               border-bottom: 3px solid #667eea;
               display: inline-block;
               padding-bottom: 10px;
           }
           .course-name {
               font-size: 28px;
               color: #764ba2;
               margin: 40px 0;
               font-weight: bold;
           }
           .completion-text {
               font-size: 18px;
               color: #666;
               margin-bottom: 60px;
               line-height: 1.8;
           }
           .footer {
               margin-top: 80px;
               padding-top: 40px;
               border-top: 2px solid #eee;
           }
           .date {
               font-size: 16px;
               color: #999;
               margin-bottom: 20px;
           }
           .code {
               font-size: 14px;
               color: #aaa;
               font-family: monospace;
           }
       </style>
   </head>
   <body>
       <div class="certificate">
           <div class="title">์ˆ˜๋ฃŒ์ฆ</div>
           <div class="subtitle">Certificate of Completion</div>
           
           <div class="recipient">{{ $userName }}</div>
           
           <div class="completion-text">
               ์œ„ ์‚ฌ๋žŒ์€ ๋‹ค์Œ ๊ฐ•์ขŒ๋ฅผ ์„ฑ์‹คํžˆ ์ˆ˜๊ฐ•ํ•˜๊ณ <br>
               ๋ชจ๋“  ๊ณผ์ •์„ ์ด์ˆ˜ํ•˜์˜€์Œ์„ ์ฆ๋ช…ํ•ฉ๋‹ˆ๋‹ค.
           </div>
           
           <div class="course-name">{{ $courseName }}</div>
           
           <div class="footer">
               <div class="date">๋ฐœ๊ธ‰์ผ: {{ $issuedDate }}</div>
               <div class="code">์ธ์ฆ๋ฒˆํ˜ธ: {{ $certificateCode }}</div>
           </div>
       </div>
   </body>
   </html>
   ```

6. `resources/js/Pages/Courses/Show.jsx` ์ˆ˜์ • (์ˆ˜๋ฃŒ์ฆ ๋ฐœ๊ธ‰ ๋ฒ„ํŠผ ์ถ”๊ฐ€):
   - ์ง„ํ–‰๋ฅ  100% ๋„๋‹ฌ ์‹œ ์ถ•ํ•˜ ๋ฉ”์‹œ์ง€์™€ ์ˆ˜๋ฃŒ์ฆ ๋ฐœ๊ธ‰ ๋ฒ„ํŠผ ํ‘œ์‹œ:
   ```jsx
   {enrollment && progress === 100 && (
       <div className="mb-8 bg-gradient-to-r from-green-50 to-blue-50 border-2 border-green-300 rounded-lg p-6">
           <div className="flex items-center justify-between">
               <div>
                   <h3 className="text-2xl font-bold text-green-700 mb-2">๐ŸŽ‰ ์ถ•ํ•˜ํ•ฉ๋‹ˆ๋‹ค!</h3>
                   <p className="text-gray-700">๋ชจ๋“  ๊ฐ•์˜๋ฅผ ์™„๋ฃŒํ•˜์…จ์Šต๋‹ˆ๋‹ค. ์ˆ˜๋ฃŒ์ฆ์„ ๋ฐœ๊ธ‰๋ฐ›์œผ์„ธ์š”.</p>
               </div>
               <button
                   onClick={() => router.post(route('certificates.generate', enrollment.id))}
                   className="bg-gradient-to-r from-green-500 to-blue-500 text-white px-6 py-3 rounded-lg hover:from-green-600 hover:to-blue-600 transition-all transform hover:scale-105 shadow-lg"
               >
                   ์ˆ˜๋ฃŒ์ฆ ๋ฐœ๊ธ‰
               </button>
           </div>
       </div>
   )}
   ```

7. React ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ: `resources/js/Pages/Certificates/Show.jsx`
   ```jsx
   import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
   import { Head, Link } from '@inertiajs/react';

   export default function Show({ auth, certificate }) {
       return (
           <AuthenticatedLayout user={auth.user}>
               <Head title="์ˆ˜๋ฃŒ์ฆ" />

               <div className="py-12">
                   <div className="max-w-4xl mx-auto sm:px-6 lg:px-8">
                       <div className="bg-gradient-to-br from-purple-600 to-blue-600 rounded-lg shadow-2xl overflow-hidden">
                           <div className="bg-white m-8 p-12 rounded-lg">
                               <div className="text-center">
                                   <h1 className="text-5xl font-bold text-purple-600 mb-8">์ˆ˜๋ฃŒ์ฆ</h1>
                                   <p className="text-xl text-gray-600 mb-12">Certificate of Completion</p>
                                   
                                   <div className="text-4xl font-bold text-gray-900 mb-4 border-b-4 border-purple-600 inline-block pb-2">
                                       {certificate.enrollment.user.name}
                                   </div>
                                   
                                   <p className="text-lg text-gray-700 my-12 leading-relaxed">
                                       ์œ„ ์‚ฌ๋žŒ์€ ๋‹ค์Œ ๊ฐ•์ขŒ๋ฅผ ์„ฑ์‹คํžˆ ์ˆ˜๊ฐ•ํ•˜๊ณ <br />
                                       ๋ชจ๋“  ๊ณผ์ •์„ ์ด์ˆ˜ํ•˜์˜€์Œ์„ ์ฆ๋ช…ํ•ฉ๋‹ˆ๋‹ค.
                                   </p>
                                   
                                   <div className="text-3xl font-bold text-blue-700 my-12">
                                       {certificate.enrollment.course.title}
                                   </div>
                                   
                                   <div className="mt-16 pt-8 border-t-2 border-gray-200">
                                       <p className="text-gray-600 mb-4">
                                           ๋ฐœ๊ธ‰์ผ: {new Date(certificate.issued_at).toLocaleDateString('ko-KR')}
                                       </p>
                                       <p className="text-gray-500 text-sm font-mono">
                                           ์ธ์ฆ๋ฒˆํ˜ธ: {certificate.certificate_code}
                                       </p>
                                   </div>
                               </div>
                           </div>
                       </div>

                       <div className="mt-8 flex justify-center space-x-4">
                           <a
                               href={route('certificates.download', certificate.id)}
                               className="bg-green-600 text-white px-8 py-4 rounded-lg hover:bg-green-700 transition-colors shadow-lg text-lg font-semibold"
                           >
                               ๐Ÿ“ฅ PDF ๋‹ค์šด๋กœ๋“œ
                           </a>
                           <Link
                               href={route('courses.index')}
                               className="bg-gray-600 text-white px-8 py-4 rounded-lg hover:bg-gray-700 transition-colors shadow-lg text-lg font-semibold"
                           >
                               ๊ฐ•์ขŒ ๋ชฉ๋ก์œผ๋กœ
                           </Link>
                       </div>
                   </div>
               </div>
           </AuthenticatedLayout>
       );
   }
   ```

์™„๋ฃŒ๋˜๋ฉด "์ˆ˜๋ฃŒ์ฆ ์ž๋™ ์ƒ์„ฑ ๋ฐ PDF ๋‹ค์šด๋กœ๋“œ ๊ธฐ๋Šฅ ์™„์„ฑ"์ด๋ผ๊ณ  ์•Œ๋ ค์ฃผ์„ธ์š”.

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: CertificateController๊ฐ€ ์ƒ์„ฑ๋˜๊ณ , 100% ์™„๋ฃŒ ์‹œ ์ˆ˜๋ฃŒ์ฆ ๋ฐœ๊ธ‰ ๋ฒ„ํŠผ์ด ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค. ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ์ˆ˜๋ฃŒ์ฆ์ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅ๋˜๊ณ , ์ˆ˜๋ฃŒ์ฆ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค. PDF ๋‹ค์šด๋กœ๋“œ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด DomPDF๋กœ ์ƒ์„ฑ๋œ ์ˆ˜๋ฃŒ์ฆ PDF ํŒŒ์ผ์ด ๋‹ค์šด๋กœ๋“œ๋ฉ๋‹ˆ๋‹ค. ์ˆ˜๋ฃŒ์ฆ์—๋Š” ์ˆ˜๊ฐ•์ƒ ์ด๋ฆ„, ๊ฐ•์ขŒ๋ช…, ๋ฐœ๊ธ‰์ผ, ์ธ์ฆ๋ฒˆํ˜ธ๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.

6
Step 6
๋งˆ์ง€๋ง‰์œผ๋กœ ์ˆ˜๋ฃŒ ์ด๋ ฅ ํŽ˜์ด์ง€๋ฅผ ๋งŒ๋“ค์–ด์„œ ์‚ฌ์šฉ์ž๊ฐ€ ์ž์‹ ์ด ์ˆ˜๋ฃŒํ•œ ๊ฐ•์ขŒ ๋ชฉ๋ก๊ณผ ์ˆ˜๋ฃŒ์ฆ์„ ํ•œ๋ˆˆ์— ๋ณผ ์ˆ˜ ์žˆ๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

1. `app/Http/Controllers/CertificateController.php`์— index ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€:
   ```php
   public function index()
   {
       $certificates = auth()->user()->enrollments()
           ->whereHas('certificate')
           ->with(['course', 'certificate'])
           ->get()
           ->map(function ($enrollment) {
               return [
                   'id' => $enrollment->certificate->id,
                   'course_title' => $enrollment->course->title,
                   'course_thumbnail' => $enrollment->course->thumbnail,
                   'instructor' => $enrollment->course->instructor,
                   'issued_at' => $enrollment->certificate->issued_at->format('Y๋…„ m์›” d์ผ'),
                   'certificate_code' => $enrollment->certificate->certificate_code,
               ];
           });

       return Inertia::render('Certificates/Index', [
           'certificates' => $certificates,
       ]);
   }
   ```

2. `routes/web.php`์— ๋ผ์šฐํŠธ ์ถ”๊ฐ€:
   ```php
   Route::middleware('auth')->group(function () {
       Route::get('/my-certificates', [CertificateController::class, 'index'])->name('certificates.index');
   });
   ```

3. React ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ: `resources/js/Pages/Certificates/Index.jsx`
   ```jsx
   import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
   import { Head, Link } from '@inertiajs/react';

   export default function Index({ auth, certificates }) {
       return (
           <AuthenticatedLayout user={auth.user}>
               <Head title="์ˆ˜๋ฃŒ ์ด๋ ฅ" />

               <div className="py-12">
                   <div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
                       <div className="mb-8">
                           <h1 className="text-3xl font-bold text-gray-900 mb-2">๋‚ด ์ˆ˜๋ฃŒ์ฆ</h1>
                           <p className="text-gray-600">์™„๋ฃŒํ•œ ๊ฐ•์ขŒ์˜ ์ˆ˜๋ฃŒ์ฆ์„ ํ™•์ธํ•˜๊ณ  ๋‹ค์šด๋กœ๋“œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.</p>
                       </div>

                       {certificates.length === 0 ? (
                           <div className="bg-white rounded-lg shadow-md p-12 text-center">
                               <svg className="mx-auto h-24 w-24 text-gray-400 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                                   <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
                               </svg>
                               <h3 className="text-xl font-semibold text-gray-900 mb-2">์•„์ง ์ˆ˜๋ฃŒํ•œ ๊ฐ•์ขŒ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค</h3>
                               <p className="text-gray-600 mb-6">๊ฐ•์ขŒ๋ฅผ ์™„๋ฃŒํ•˜๊ณ  ์ˆ˜๋ฃŒ์ฆ์„ ๋ฐ›์•„๋ณด์„ธ์š”!</p>
                               <Link
                                   href={route('courses.index')}
                                   className="inline-block bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 transition-colors"
                               >
                                   ๊ฐ•์ขŒ ๋‘˜๋Ÿฌ๋ณด๊ธฐ
                               </Link>
                           </div>
                       ) : (
                           <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
                               {certificates.map((cert) => (
                                   <div
                                       key={cert.id}
                                       className="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-xl transition-shadow duration-300"
                                   >
                                       <div className="relative">
                                           <img
                                               src={cert.course_thumbnail}
                                               alt={cert.course_title}
                                               className="w-full h-48 object-cover"
                                           />
                                           <div className="absolute top-4 right-4 bg-green-500 text-white px-3 py-1 rounded-full text-sm font-semibold">
                                               โœ“ ์ˆ˜๋ฃŒ ์™„๋ฃŒ
                                           </div>
                                       </div>
                                       <div className="p-6">
                                           <h2 className="text-xl font-semibold text-gray-900 mb-2">
                                               {cert.course_title}
                                           </h2>
                                           <p className="text-gray-600 text-sm mb-4">
                                               ๊ฐ•์‚ฌ: {cert.instructor}
                                           </p>
                                           <div className="border-t border-gray-200 pt-4 mb-4">
                                               <p className="text-gray-600 text-sm mb-1">
                                                   ๋ฐœ๊ธ‰์ผ: {cert.issued_at}
                                               </p>
                                               <p className="text-gray-500 text-xs font-mono">
                                                   ์ธ์ฆ๋ฒˆํ˜ธ: {cert.certificate_code}
                                               </p>
                                           </div>
                                           <div className="flex space-x-2">
                                               <Link
                                                   href={route('certificates.show', cert.id)}
                                                   className="flex-1 bg-blue-600 text-white text-center px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors text-sm font-medium"
                                               >
                                                   ๋ฏธ๋ฆฌ๋ณด๊ธฐ
                                               </Link>
                                               <a
                                                   href={route('certificates.download', cert.id)}
                                                   className="flex-1 bg-green-600 text-white text-center px-4 py-2 rounded-lg hover:bg-green-700 transition-colors text-sm font-medium"
                                               >
                                                   PDF ๋‹ค์šด
                                               </a>
                                           </div>
                                       </div>
                                   </div>
                               ))}
                           </div>
                       )}
                   </div>
               </div>
           </AuthenticatedLayout>
       );
   }
   ```

4. ๋„ค๋น„๊ฒŒ์ด์…˜์— "๋‚ด ์ˆ˜๋ฃŒ์ฆ" ๋งํฌ ์ถ”๊ฐ€: `resources/js/Layouts/AuthenticatedLayout.jsx`์˜ NavLink ์„น์…˜์— ์ถ”๊ฐ€:
   ```jsx
   <NavLink href={route('certificates.index')} active={route().current('certificates.index')}>
       ๋‚ด ์ˆ˜๋ฃŒ์ฆ
   </NavLink>
   ```

5. ํ”„๋กœ์ ํŠธ ์ตœ์ข… ํ…Œ์ŠคํŠธ:
   - ํšŒ์›๊ฐ€์ž… ํ›„ ๋กœ๊ทธ์ธ
   - ๊ฐ•์ขŒ ๋ชฉ๋ก์—์„œ ๊ฐ•์ขŒ ํด๋ฆญ
   - "์ˆ˜๊ฐ• ์‹œ์ž‘ํ•˜๊ธฐ" ๋ฒ„ํŠผ์œผ๋กœ ์ˆ˜๊ฐ• ๋“ฑ๋ก
   - ๊ฐ ๊ฐ•์˜ ์˜† ์ฒดํฌ๋ฐ•์Šค๋ฅผ ํด๋ฆญํ•˜์—ฌ ์™„๋ฃŒ ํ‘œ์‹œ
   - ์ง„ํ–‰๋ฅ ์ด 100%์— ๋„๋‹ฌํ•˜๋ฉด "์ˆ˜๋ฃŒ์ฆ ๋ฐœ๊ธ‰" ๋ฒ„ํŠผ ํด๋ฆญ
   - ์ˆ˜๋ฃŒ์ฆ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ํ™•์ธ
   - PDF ๋‹ค์šด๋กœ๋“œ ๋ฒ„ํŠผ์œผ๋กœ ์ˆ˜๋ฃŒ์ฆ ๋‹ค์šด๋กœ๋“œ
   - "๋‚ด ์ˆ˜๋ฃŒ์ฆ" ๋ฉ”๋‰ด์—์„œ ์ˆ˜๋ฃŒ ์ด๋ ฅ ํ™•์ธ

6. (์„ ํƒ) README.md ์ž‘์„ฑ:
   ```markdown
   # ์˜จ๋ผ์ธ ๊ฐ•์ขŒ ์ˆ˜๋ฃŒ์ฆ ์ž๋™ ๋ฐœ๊ธ‰ ์‹œ์Šคํ…œ

   Laravel 13.x + Inertia.js + React 19๋กœ ๋งŒ๋“  ์˜จ๋ผ์ธ ๊ต์œก ํ”Œ๋žซํผ์ž…๋‹ˆ๋‹ค.

   ## ์ฃผ์š” ๊ธฐ๋Šฅ
   - ๊ฐ•์ขŒ ๋ชฉ๋ก ๋ฐ ์ƒ์„ธ ์ •๋ณด ํ™•์ธ
   - ๊ฐ•์˜ ์˜์ƒ ์‹œ์ฒญ ์ฒดํฌ๋ฆฌ์ŠคํŠธ
   - ์‹ค์‹œ๊ฐ„ ์ง„ํ–‰๋ฅ  ์ถ”์ 
   - 100% ์™„๋ฃŒ ์‹œ ์ˆ˜๋ฃŒ์ฆ ์ž๋™ ๋ฐœ๊ธ‰
   - PDF ์ˆ˜๋ฃŒ์ฆ ๋‹ค์šด๋กœ๋“œ
   - ์ˆ˜๋ฃŒ ์ด๋ ฅ ๊ด€๋ฆฌ

   ## ์„ค์น˜ ๋ฐฉ๋ฒ•
   ```bash
   composer install
   npm install
   cp .env.example .env
   touch database/database.sqlite
   php artisan key:generate
   php artisan migrate
   php artisan db:seed
   npm run dev
   php artisan serve
   ```

   ## ์‚ฌ์šฉ ๊ธฐ์ˆ 
   - Laravel 13.x
   - Inertia.js
   - React 19
   - Tailwind CSS
   - DomPDF
   ```

์™„๋ฃŒ๋˜๋ฉด "ํ”„๋กœ์ ํŠธ ์™„์„ฑ! ์ˆ˜๋ฃŒ ์ด๋ ฅ ํŽ˜์ด์ง€๊นŒ์ง€ ๋ชจ๋“  ๊ธฐ๋Šฅ ๊ตฌํ˜„ ์™„๋ฃŒ"๋ผ๊ณ  ์•Œ๋ ค์ฃผ์„ธ์š”.

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ์ˆ˜๋ฃŒ ์ด๋ ฅ ํŽ˜์ด์ง€๊ฐ€ ์ƒ์„ฑ๋˜์–ด ์‚ฌ์šฉ์ž๊ฐ€ ์ž์‹ ์ด ์ˆ˜๋ฃŒํ•œ ๋ชจ๋“  ๊ฐ•์ขŒ์˜ ์ˆ˜๋ฃŒ์ฆ์„ ์นด๋“œ ํ˜•ํƒœ๋กœ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐ ์นด๋“œ์—์„œ ์ˆ˜๋ฃŒ์ฆ ๋ฏธ๋ฆฌ๋ณด๊ธฐ์™€ PDF ๋‹ค์šด๋กœ๋“œ๊ฐ€ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ์•„์ง ์ˆ˜๋ฃŒํ•œ ๊ฐ•์ขŒ๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€์™€ ๊ฐ•์ขŒ ๋ชฉ๋ก ๋งํฌ๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. ๋ชจ๋“  ๊ธฐ๋Šฅ์ด ์ •์ƒ ์ž‘๋™ํ•˜๋ฉฐ, ํ”„๋กœ์ ํŠธ๊ฐ€ ์™„์„ฑ๋ฉ๋‹ˆ๋‹ค.

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

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

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

๋Œ“๊ธ€ (0)

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