๐ŸŽ“ ์˜จ๋ผ์ธ ์ž๊ฒฉ์ฆ ์‹œํ—˜ ์‘์‹œ ํ”Œ๋žซํผ - ์‹ค์‹œ๊ฐ„ ํƒ€์ด๋จธ & ์ž๋™ ์ฑ„์  ์‹œ์Šคํ…œ - ๋ฐ”์ด๋ธŒ์ฝ”๋”ฉ ๋ ˆ์‹œํ”ผ
๊ณ ๊ธ‰ PHP Laravel AI: Claude

๐ŸŽ“ ์˜จ๋ผ์ธ ์ž๊ฒฉ์ฆ ์‹œํ—˜ ์‘์‹œ ํ”Œ๋žซํผ - ์‹ค์‹œ๊ฐ„ ํƒ€์ด๋จธ & ์ž๋™ ์ฑ„์  ์‹œ์Šคํ…œ

๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋ช… online-certification-exam-platform
์กฐํšŒ์ˆ˜ 61 0๊ฑด ์ œ์ถœ

๋ฌธ์ œ ์„ค๋ช…

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

์‹ค์ œ ์ž๊ฒฉ์ฆ ์‹œํ—˜์ฒ˜๋Ÿผ ์ œํ•œ ์‹œ๊ฐ„ ๋‚ด์— ๋ฌธ์ œ๋ฅผ ํ’€๊ณ  ์ž๋™์œผ๋กœ ์ฑ„์ ๋˜๋Š” ์˜จ๋ผ์ธ ์‹œํ—˜ ์‘์‹œ ํ”Œ๋žซํผ์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค. ๊ด€๋ฆฌ์ž๋Š” ์‹œํ—˜์„ ์ƒ์„ฑํ•˜๊ณ  ๋ฌธ์ œ๋ฅผ ๋“ฑ๋กํ•˜๋ฉฐ, ์‘์‹œ์ž๋Š” ์‹ค์‹œ๊ฐ„ ํƒ€์ด๋จธ๋ฅผ ๋ณด๋ฉด์„œ ์‹œํ—˜์„ ์น˜๋ฅด๊ณ  ์ฆ‰์‹œ ๊ฒฐ๊ณผ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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

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

  • โœ… ์‹œํ—˜ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ: ๊ด€๋ฆฌ์ž๊ฐ€ ์‹œํ—˜ ์ƒ์„ฑ (์ œ๋ชฉ, ์„ค๋ช…, ์ œํ•œ์‹œ๊ฐ„, ํ•ฉ๊ฒฉ์ ์ˆ˜, ๊ณต๊ฐœ/๋น„๊ณต๊ฐœ ์„ค์ •)
  • โœ… ๋ฌธ์ œ ์€ํ–‰: ๊ฐ๊ด€์‹/์ฃผ๊ด€์‹ ๋ฌธ์ œ ๋“ฑ๋ก (์ •๋‹ต, ๋ฐฐ์ , ์นดํ…Œ๊ณ ๋ฆฌ ์„ค์ •)
  • โœ… ์‘์‹œ์ž ๋Œ€์‹œ๋ณด๋“œ: ๊ณต๊ฐœ๋œ ์‹œํ—˜ ๋ชฉ๋ก ์กฐํšŒ ๋ฐ ์‘์‹œ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ ํ™•์ธ
  • โœ… ์‹ค์‹œ๊ฐ„ ํƒ€์ด๋จธ: JavaScript๋กœ ๊ตฌํ˜„๋œ ์นด์šดํŠธ๋‹ค์šด ํƒ€์ด๋จธ (์‹œ๊ฐ„ ์ดˆ๊ณผ ์‹œ ์ž๋™ ์ œ์ถœ)
  • โœ… ์‹œํ—˜ ์‘์‹œ ํŽ˜์ด์ง€: ๋ฌธ์ œ ๋„ค๋น„๊ฒŒ์ด์…˜, ๋‹ต์•ˆ ์ž„์‹œ ์ €์žฅ, ์ง„ํ–‰๋ฅ  ํ‘œ์‹œ
  • โœ… ์ž๋™ ์ฑ„์ : ์ œ์ถœ ์ฆ‰์‹œ ๊ฐ๊ด€์‹ ์ž๋™ ์ฑ„์  ๋ฐ ์ ์ˆ˜ ๊ณ„์‚ฐ
  • โœ… ๊ฒฐ๊ณผ ๋ฆฌํฌํŠธ: ์ด์ , ํ•ฉ๊ฒฉ/๋ถˆํ•ฉ๊ฒฉ, ๋ฌธ์ œ๋ณ„ ์ •์˜ค๋‹ต ํ‘œ์‹œ, ์ •๋‹ต๋ฅ  ํ†ต๊ณ„
  • โœ… ์‘์‹œ ๊ธฐ๋ก: ์‚ฌ์šฉ์ž๋ณ„ ์‹œํ—˜ ์‘์‹œ ํžˆ์Šคํ† ๋ฆฌ ๋ฐ ์žฌ์‘์‹œ ์ œํ•œ ์„ค์ •
  • โœ… ์ธ์ฆ ์‹œ์Šคํ…œ: Laravel Breeze ๊ธฐ๋ฐ˜ ํšŒ์›๊ฐ€์ž…/๋กœ๊ทธ์ธ (์—ญํ• : ๊ด€๋ฆฌ์ž/์‘์‹œ์ž)

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

  • ๐ŸŽ ์‹œํ—˜ ์นดํ…Œ๊ณ ๋ฆฌ: ํ”„๋กœ๊ทธ๋ž˜๋ฐ, ๋””์ž์ธ, ๋งˆ์ผ€ํŒ… ๋“ฑ ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์‹œํ—˜ ๋ถ„๋ฅ˜
  • ๐ŸŽ ๋‚œ์ด๋„ ํ•„ํ„ฐ: ์ดˆ๊ธ‰/์ค‘๊ธ‰/๊ณ ๊ธ‰ ๋‚œ์ด๋„๋ณ„ ์‹œํ—˜ ๊ฒ€์ƒ‰
  • ๐ŸŽ ๋ฌธ์ œ ๋žœ๋ค ์ถœ์ œ: ๋ฌธ์ œ ์€ํ–‰์—์„œ ๋ฌด์ž‘์œ„๋กœ N๊ฐœ ์ถœ์ œ (๋งค๋ฒˆ ๋‹ค๋ฅธ ์‹œํ—˜์ง€)
  • ๐ŸŽ ํ•ฉ๊ฒฉ์ฆ PDF ๋ฐœ๊ธ‰: ํ•ฉ๊ฒฉ ์‹œ PDF ์ธ์ฆ์„œ ์ž๋™ ์ƒ์„ฑ (Laravel DomPDF)
  • ๐ŸŽ ์‹œํ—˜ ํ†ต๊ณ„ ๋Œ€์‹œ๋ณด๋“œ: ์‘์‹œ์ž ์ˆ˜, ํ‰๊ท  ์ ์ˆ˜, ํ•ฉ๊ฒฉ๋ฅ  ์ฐจํŠธ (Chart.js)
  • ๐ŸŽ ์ด๋ฉ”์ผ ์•Œ๋ฆผ: ์‹œํ—˜ ๊ฒฐ๊ณผ ๋ฐ ํ•ฉ๊ฒฉ์ฆ ์ด๋ฉ”์ผ ๋ฐœ์†ก (Laravel Mail)
  • ๐ŸŽ ๋ถ€์ •ํ–‰์œ„ ๋ฐฉ์ง€: ํƒญ ์ „ํ™˜ ๊ฐ์ง€ ๋ฐ ๊ฒฝ๊ณ  (JavaScript Visibility API)
  • ๐ŸŽ ๋‹คํฌ๋ชจ๋“œ: Tailwind CSS ๋‹คํฌ๋ชจ๋“œ ํ† ๊ธ€

๊ธฐ์ˆ  ์Šคํƒ

  • ๋ฐฑ์—”๋“œ: Laravel 13.x + Blade ํ…œํ”Œ๋ฆฟ
  • ํ”„๋ก ํŠธ์—”๋“œ: Tailwind CSS 4.x + Alpine.js (๊ฒฝ๋Ÿ‰ ์ธํ„ฐ๋ž™์…˜)
  • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค: SQLite (๊ฐœ๋ฐœ) / MySQL (ํ”„๋กœ๋•์…˜)
  • ์ธ์ฆ: Laravel Breeze (Blade ์Šคํƒ)
  • ์ถ”๊ฐ€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ: Chart.js (ํ†ต๊ณ„), DomPDF (PDF ์ƒ์„ฑ)

์ฃผ์š” ํ™”๋ฉด ๊ตฌ์„ฑ

  1. ํ™ˆ: ๊ณต๊ฐœ ์‹œํ—˜ ๋ชฉ๋ก (์นด๋“œ ๋ ˆ์ด์•„์›ƒ, ๋‚œ์ด๋„/์นดํ…Œ๊ณ ๋ฆฌ ๋ฐฐ์ง€)
  2. ์‹œํ—˜ ์ƒ์„ธ: ์‹œํ—˜ ์ •๋ณด, ์‘์‹œ ์กฐ๊ฑด, ์‘์‹œ ๋ฒ„ํŠผ
  3. ์‹œํ—˜ ์‘์‹œ: ๋ฌธ์ œ ๋ฆฌ์ŠคํŠธ, ์‹ค์‹œ๊ฐ„ ํƒ€์ด๋จธ, ๋‹ต์•ˆ ์„ ํƒ UI
  4. ๊ฒฐ๊ณผ ํŽ˜์ด์ง€: ์ ์ˆ˜, ํ•ฉ๊ฒฉ/๋ถˆํ•ฉ๊ฒฉ, ๋ฌธ์ œ๋ณ„ ์ •์˜ค๋‹ต ํ•ด์„ค
  5. ๋งˆ์ดํŽ˜์ด์ง€: ์‘์‹œ ๊ธฐ๋ก, ํ•ฉ๊ฒฉ์ฆ ๋‹ค์šด๋กœ๋“œ
  6. ๊ด€๋ฆฌ์ž ํŒจ๋„: ์‹œํ—˜/๋ฌธ์ œ CRUD, ์‘์‹œ์ž ํ†ต๊ณ„

์ œ์•ฝ ์กฐ๊ฑด

  • โ€ข Laravel 13.x + Blade ํ…œํ”Œ๋ฆฟ ์‚ฌ์šฉ (React/Vue ๊ธˆ์ง€)
  • โ€ข Tailwind CSS 4.x๋กœ ๋ฐ˜์‘ํ˜• UI ๊ตฌํ˜„
  • โ€ข Alpine.js๋กœ ๊ฒฝ๋Ÿ‰ ์ธํ„ฐ๋ž™์…˜ ์ฒ˜๋ฆฌ (ํƒ€์ด๋จธ, ๋ชจ๋‹ฌ, ํ† ๊ธ€)
  • โ€ข Laravel Breeze Blade ์Šคํƒ์œผ๋กœ ์ธ์ฆ ๊ตฌํ˜„
  • โ€ข SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์‚ฌ์šฉ (๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ•„์ˆ˜)
  • โ€ข Eloquent ORM์œผ๋กœ ๊ด€๊ณ„ํ˜• ๋ฐ์ดํ„ฐ ์„ค๊ณ„ (์‹œํ—˜-๋ฌธ์ œ-์‘์‹œ๊ธฐ๋ก 1:N ๊ด€๊ณ„)
  • โ€ข Laravel Policy๋กœ ๊ถŒํ•œ ์ œ์–ด (๊ด€๋ฆฌ์ž๋งŒ ์‹œํ—˜ ์ƒ์„ฑ/์ˆ˜์ • ๊ฐ€๋Šฅ)
  • โ€ข JavaScript Fetch API๋กœ AJAX ๋‹ต์•ˆ ์ž„์‹œ ์ €์žฅ
  • โ€ข Laravel Validation์œผ๋กœ ์ž…๋ ฅ๊ฐ’ ๊ฒ€์ฆ
  • โ€ข Blade ์ปดํฌ๋„ŒํŠธ๋กœ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ UI ๋ชจ๋“ˆํ™”
  • โ€ข ์‹ค์‹œ๊ฐ„ ํƒ€์ด๋จธ๋Š” JavaScript setInterval๋กœ ๊ตฌํ˜„ (์„œ๋ฒ„ ํƒ€์ž„์Šคํƒฌํ”„ ๊ธฐ์ค€)
  • โ€ข ์‹œํ—˜ ์ œ์ถœ ์‹œ CSRF ํ† ํฐ ๊ฒ€์ฆ ํ•„์ˆ˜

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

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

1
Step 1
mkdir online-certification-exam-platform && cd online-certification-exam-platform

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

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

2. Laravel Breeze (Blade ์Šคํƒ) ์„ค์น˜ ๋ฐ ์ธ์ฆ ์Šค์บํด๋”ฉ ์ƒ์„ฑ:
```bash
composer require laravel/breeze --dev
php artisan breeze:install blade
php artisan migrate
npm install && npm run dev
```

3. Tailwind CSS 4.x ์„ค์ • ํ™•์ธ (Breeze๊ฐ€ ์ž๋™ ์„ค์ •):
- `tailwind.config.js` ํŒŒ์ผ์ด ์ƒ์„ฑ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ
- `resources/css/app.css`์— Tailwind ๋””๋ ‰ํ‹ฐ๋ธŒ๊ฐ€ ํฌํ•จ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ

4. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ • (SQLite):
- `.env` ํŒŒ์ผ์—์„œ `DB_CONNECTION=sqlite` ์„ค์ •
- `touch database/database.sqlite` ๋ช…๋ น์œผ๋กœ SQLite ํŒŒ์ผ ์ƒ์„ฑ

5. ๊ธฐ๋ณธ ๋ผ์šฐํŠธ ํ™•์ธ:
- `routes/web.php`์—์„œ Breeze๊ฐ€ ์ƒ์„ฑํ•œ ์ธ์ฆ ๋ผ์šฐํŠธ ํ™•์ธ
- `php artisan route:list` ์‹คํ–‰ํ•˜์—ฌ ๋ผ์šฐํŠธ ๋ชฉ๋ก ํ™•์ธ

6. ๊ฐœ๋ฐœ ์„œ๋ฒ„ ์‹คํ–‰:
```bash
php artisan serve
```

๋ธŒ๋ผ์šฐ์ €์—์„œ `http://localhost:8000` ์ ‘์†ํ•˜์—ฌ Breeze ๊ธฐ๋ณธ ๋ ˆ์ด์•„์›ƒ ํ™•์ธ.

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: Laravel 13.x ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ ์™„๋ฃŒ. Breeze Blade ์Šคํƒ ์„ค์น˜๋กœ ํšŒ์›๊ฐ€์ž…/๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ ์‚ฌ์šฉ ๊ฐ€๋Šฅ. Tailwind CSS 4.x ์„ค์ • ์™„๋ฃŒ. SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ์™„๋ฃŒ. ๊ฐœ๋ฐœ ์„œ๋ฒ„ ์‹คํ–‰ ์‹œ Breeze ๊ธฐ๋ณธ ํ™ˆ ํ™”๋ฉด ํ‘œ์‹œ.

2
Step 2
๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ ์„ค๊ณ„ ๋ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ƒ์„ฑ:

1. **User ๋ชจ๋ธ ํ™•์žฅ** (๊ด€๋ฆฌ์ž/์‘์‹œ์ž ์—ญํ•  ๊ตฌ๋ถ„):
```bash
php artisan make:migration add_role_to_users_table --table=users
```
๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ์—์„œ:
```php
$table->enum('role', ['admin', 'examinee'])->default('examinee');
```

2. **Exam ๋ชจ๋ธ** (์‹œํ—˜ ์ •๋ณด):
```bash
php artisan make:model Exam -m
```
๋งˆ์ด๊ทธ๋ ˆ์ด์…˜:
```php
$table->id();
$table->string('title'); // ์‹œํ—˜ ์ œ๋ชฉ
$table->text('description')->nullable(); // ์‹œํ—˜ ์„ค๋ช…
$table->integer('duration_minutes'); // ์ œํ•œ ์‹œ๊ฐ„ (๋ถ„)
$table->integer('passing_score'); // ํ•ฉ๊ฒฉ ์ ์ˆ˜
$table->enum('status', ['draft', 'published'])->default('draft'); // ๊ณต๊ฐœ ์—ฌ๋ถ€
$table->string('category')->nullable(); // ์นดํ…Œ๊ณ ๋ฆฌ (ํ”„๋กœ๊ทธ๋ž˜๋ฐ, ๋””์ž์ธ ๋“ฑ)
$table->enum('difficulty', ['beginner', 'intermediate', 'advanced'])->default('beginner'); // ๋‚œ์ด๋„
$table->boolean('allow_retake')->default(true); // ์žฌ์‘์‹œ ํ—ˆ์šฉ ์—ฌ๋ถ€
$table->timestamps();
```

3. **Question ๋ชจ๋ธ** (๋ฌธ์ œ):
```bash
php artisan make:model Question -m
```
๋งˆ์ด๊ทธ๋ ˆ์ด์…˜:
```php
$table->id();
$table->foreignId('exam_id')->constrained()->onDelete('cascade');
$table->text('question_text'); // ๋ฌธ์ œ ๋‚ด์šฉ
$table->enum('question_type', ['multiple_choice', 'short_answer'])->default('multiple_choice');
$table->json('options')->nullable(); // ๊ฐ๊ด€์‹ ์„ ํƒ์ง€ (JSON ๋ฐฐ์—ด)
$table->string('correct_answer'); // ์ •๋‹ต
$table->integer('points')->default(1); // ๋ฐฐ์ 
$table->integer('order')->default(0); // ๋ฌธ์ œ ์ˆœ์„œ
$table->timestamps();
```

4. **ExamAttempt ๋ชจ๋ธ** (์‘์‹œ ๊ธฐ๋ก):
```bash
php artisan make:model ExamAttempt -m
```
๋งˆ์ด๊ทธ๋ ˆ์ด์…˜:
```php
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->foreignId('exam_id')->constrained()->onDelete('cascade');
$table->timestamp('started_at')->nullable(); // ์‹œํ—˜ ์‹œ์ž‘ ์‹œ๊ฐ„
$table->timestamp('submitted_at')->nullable(); // ์ œ์ถœ ์‹œ๊ฐ„
$table->json('answers')->nullable(); // ์‚ฌ์šฉ์ž ๋‹ต์•ˆ (JSON)
$table->integer('score')->nullable(); // ์ด์ 
$table->boolean('passed')->default(false); // ํ•ฉ๊ฒฉ ์—ฌ๋ถ€
$table->timestamps();
```

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

6. ๋ชจ๋ธ ๊ด€๊ณ„ ์„ค์ •:
- `app/Models/Exam.php`์— `questions()`, `attempts()` ๊ด€๊ณ„ ์ถ”๊ฐ€
- `app/Models/Question.php`์— `exam()` ๊ด€๊ณ„ ์ถ”๊ฐ€
- `app/Models/ExamAttempt.php`์— `user()`, `exam()` ๊ด€๊ณ„ ์ถ”๊ฐ€
- `app/Models/User.php`์— `examAttempts()` ๊ด€๊ณ„ ์ถ”๊ฐ€

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ…Œ์ด๋ธ” ์ƒ์„ฑ ์™„๋ฃŒ: users (role ์ปฌ๋Ÿผ ์ถ”๊ฐ€), exams, questions, exam_attempts. Eloquent ๋ชจ๋ธ ๊ฐ„ ๊ด€๊ณ„ ์„ค์ • ์™„๋ฃŒ (1:N, N:1). ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์„ฑ๊ณต์ ์œผ๋กœ ์‹คํ–‰๋จ.

3
Step 3
์‹œ๋”(Seeder)๋กœ ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ:

1. **DatabaseSeeder ์ž‘์„ฑ**:
```bash
php artisan make:seeder ExamSeeder
```

`database/seeders/ExamSeeder.php`:
```php
use App\Models\Exam;
use App\Models\Question;
use App\Models\User;

public function run()
{
    // ๊ด€๋ฆฌ์ž ๊ณ„์ • ์ƒ์„ฑ
    User::create([
        'name' => 'Admin User',
        'email' => 'admin@example.com',
        'password' => bcrypt('password'),
        'role' => 'admin',
    ]);

    // ์‘์‹œ์ž ๊ณ„์ • ์ƒ์„ฑ
    User::create([
        'name' => 'Test Examinee',
        'email' => 'examinee@example.com',
        'password' => bcrypt('password'),
        'role' => 'examinee',
    ]);

    // ์ƒ˜ํ”Œ ์‹œํ—˜ 1: Laravel ๊ธฐ์ดˆ
    $exam1 = Exam::create([
        'title' => 'Laravel ๊ธฐ์ดˆ ์ž๊ฒฉ์ฆ ์‹œํ—˜',
        'description' => 'Laravel ํ”„๋ ˆ์ž„์›Œํฌ์˜ ๊ธฐ๋ณธ ๊ฐœ๋…์„ ํ…Œ์ŠคํŠธํ•ฉ๋‹ˆ๋‹ค.',
        'duration_minutes' => 30,
        'passing_score' => 70,
        'status' => 'published',
        'category' => 'ํ”„๋กœ๊ทธ๋ž˜๋ฐ',
        'difficulty' => 'beginner',
        'allow_retake' => true,
    ]);

    Question::create([
        'exam_id' => $exam1->id,
        'question_text' => 'Laravel์˜ ORM ์ด๋ฆ„์€ ๋ฌด์—‡์ธ๊ฐ€์š”?',
        'question_type' => 'multiple_choice',
        'options' => json_encode(['Doctrine', 'Eloquent', 'Propel', 'ActiveRecord']),
        'correct_answer' => 'Eloquent',
        'points' => 10,
        'order' => 1,
    ]);

    Question::create([
        'exam_id' => $exam1->id,
        'question_text' => 'Blade ํ…œํ”Œ๋ฆฟ ์—”์ง„์˜ ํ™•์žฅ์ž๋Š”?',
        'question_type' => 'multiple_choice',
        'options' => json_encode(['.tpl', '.blade.php', '.php', '.html']),
        'correct_answer' => '.blade.php',
        'points' => 10,
        'order' => 2,
    ]);

    // ์ƒ˜ํ”Œ ์‹œํ—˜ 2: JavaScript ์ค‘๊ธ‰
    $exam2 = Exam::create([
        'title' => 'JavaScript ES6+ ์ž๊ฒฉ์ฆ',
        'description' => '์ตœ์‹  JavaScript ๋ฌธ๋ฒ•๊ณผ ๊ธฐ๋Šฅ์„ ํ‰๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.',
        'duration_minutes' => 45,
        'passing_score' => 80,
        'status' => 'published',
        'category' => 'ํ”„๋กœ๊ทธ๋ž˜๋ฐ',
        'difficulty' => 'intermediate',
        'allow_retake' => false,
    ]);

    Question::create([
        'exam_id' => $exam2->id,
        'question_text' => 'Promise์˜ ์„ธ ๊ฐ€์ง€ ์ƒํƒœ๊ฐ€ ์•„๋‹Œ ๊ฒƒ์€?',
        'question_type' => 'multiple_choice',
        'options' => json_encode(['pending', 'fulfilled', 'rejected', 'completed']),
        'correct_answer' => 'completed',
        'points' => 15,
        'order' => 1,
    ]);
}
```

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

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

4. ๋ฐ์ดํ„ฐ ํ™•์ธ:
- Tinker๋กœ ํ™•์ธ: `php artisan tinker` โ†’ `Exam::with('questions')->get()`
- ๋˜๋Š” DB ๋ธŒ๋ผ์šฐ์ € ๋„๊ตฌ๋กœ SQLite ํŒŒ์ผ ์—ด๊ธฐ

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ์™„๋ฃŒ: ๊ด€๋ฆฌ์ž ๊ณ„์ • 1๊ฐœ, ์‘์‹œ์ž ๊ณ„์ • 1๊ฐœ, ์‹œํ—˜ 2๊ฐœ (๊ฐ๊ฐ ๋ฌธ์ œ 2~3๊ฐœ ํฌํ•จ). ์‹œ๋” ์‹คํ–‰ ์„ฑ๊ณต.

4
Step 4
์‘์‹œ์ž์šฉ ํ™ˆ ํ™”๋ฉด ๋ฐ ์‹œํ—˜ ๋ชฉ๋ก ํŽ˜์ด์ง€ ๊ตฌํ˜„:

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

`app/Http/Controllers/ExamController.php`:
```php
use App\Models\Exam;

public function index(Request $request)
{
    $query = Exam::where('status', 'published');

    // ์นดํ…Œ๊ณ ๋ฆฌ ํ•„ํ„ฐ
    if ($request->filled('category')) {
        $query->where('category', $request->category);
    }

    // ๋‚œ์ด๋„ ํ•„ํ„ฐ
    if ($request->filled('difficulty')) {
        $query->where('difficulty', $request->difficulty);
    }

    $exams = $query->withCount('questions')->latest()->paginate(9);
    $categories = Exam::distinct()->pluck('category');

    return view('exams.index', compact('exams', 'categories'));
}

public function show(Exam $exam)
{
    if ($exam->status !== 'published') {
        abort(403, '๊ณต๊ฐœ๋˜์ง€ ์•Š์€ ์‹œํ—˜์ž…๋‹ˆ๋‹ค.');
    }

    $exam->load('questions');
    $userAttempts = auth()->check() 
        ? auth()->user()->examAttempts()->where('exam_id', $exam->id)->get() 
        : collect();

    return view('exams.show', compact('exam', 'userAttempts'));
}
```

2. **๋ผ์šฐํŠธ ์„ค์ •** (`routes/web.php`):
```php
use App\Http\Controllers\ExamController;

Route::get('/', [ExamController::class, 'index'])->name('home');
Route::get('/exams/{exam}', [ExamController::class, 'show'])->name('exams.show');
```

3. **Blade ๋ทฐ ์ƒ์„ฑ**:

`resources/views/exams/index.blade.php`:
```blade
<x-app-layout>
    <div class="py-12">
        <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
            <h1 class="text-3xl font-bold mb-8">๐Ÿ“š ์˜จ๋ผ์ธ ์ž๊ฒฉ์ฆ ์‹œํ—˜</h1>

            <!-- ํ•„ํ„ฐ -->
            <form method="GET" class="mb-8 flex gap-4">
                <select name="category" class="rounded-md border-gray-300">
                    <option value="">์ „์ฒด ์นดํ…Œ๊ณ ๋ฆฌ</option>
                    @foreach($categories as $cat)
                        <option value="{{ $cat }}" {{ request('category') == $cat ? 'selected' : '' }}>
                            {{ $cat }}
                        </option>
                    @endforeach
                </select>
                <select name="difficulty" class="rounded-md border-gray-300">
                    <option value="">์ „์ฒด ๋‚œ์ด๋„</option>
                    <option value="beginner" {{ request('difficulty') == 'beginner' ? 'selected' : '' }}>์ดˆ๊ธ‰</option>
                    <option value="intermediate" {{ request('difficulty') == 'intermediate' ? 'selected' : '' }}>์ค‘๊ธ‰</option>
                    <option value="advanced" {{ request('difficulty') == 'advanced' ? 'selected' : '' }}>๊ณ ๊ธ‰</option>
                </select>
                <button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded-md">ํ•„ํ„ฐ ์ ์šฉ</button>
            </form>

            <!-- ์‹œํ—˜ ์นด๋“œ ๊ทธ๋ฆฌ๋“œ -->
            <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
                @foreach($exams as $exam)
                    <div class="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition">
                        <div class="flex items-center justify-between mb-4">
                            <span class="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded">{{ $exam->category }}</span>
                            <span class="text-xs text-gray-500">{{ $exam->difficulty }}</span>
                        </div>
                        <h2 class="text-xl font-bold mb-2">{{ $exam->title }}</h2>
                        <p class="text-gray-600 text-sm mb-4">{{ Str::limit($exam->description, 100) }}</p>
                        <div class="flex items-center justify-between text-sm text-gray-500 mb-4">
                            <span>โฑ๏ธ {{ $exam->duration_minutes }}๋ถ„</span>
                            <span>๐Ÿ“ {{ $exam->questions_count }}๋ฌธ์ œ</span>
                            <span>โœ… {{ $exam->passing_score }}์ </span>
                        </div>
                        <a href="{{ route('exams.show', $exam) }}" class="block bg-blue-600 text-white text-center py-2 rounded-md hover:bg-blue-700">
                            ์‹œํ—˜ ๋ณด๊ธฐ
                        </a>
                    </div>
                @endforeach
            </div>

            <div class="mt-8">
                {{ $exams->links() }}
            </div>
        </div>
    </div>
</x-app-layout>
```

`resources/views/exams/show.blade.php`:
```blade
<x-app-layout>
    <div class="py-12">
        <div class="max-w-4xl mx-auto sm:px-6 lg:px-8">
            <div class="bg-white rounded-lg shadow-md p-8">
                <h1 class="text-3xl font-bold mb-4">{{ $exam->title }}</h1>
                <p class="text-gray-600 mb-6">{{ $exam->description }}</p>

                <div class="grid grid-cols-2 gap-4 mb-8">
                    <div class="border-l-4 border-blue-500 pl-4">
                        <p class="text-sm text-gray-500">์ œํ•œ ์‹œ๊ฐ„</p>
                        <p class="text-xl font-bold">{{ $exam->duration_minutes }}๋ถ„</p>
                    </div>
                    <div class="border-l-4 border-green-500 pl-4">
                        <p class="text-sm text-gray-500">ํ•ฉ๊ฒฉ ์ ์ˆ˜</p>
                        <p class="text-xl font-bold">{{ $exam->passing_score }}์ </p>
                    </div>
                    <div class="border-l-4 border-purple-500 pl-4">
                        <p class="text-sm text-gray-500">์ด ๋ฌธ์ œ ์ˆ˜</p>
                        <p class="text-xl font-bold">{{ $exam->questions->count() }}๋ฌธ์ œ</p>
                    </div>
                    <div class="border-l-4 border-orange-500 pl-4">
                        <p class="text-sm text-gray-500">์žฌ์‘์‹œ</p>
                        <p class="text-xl font-bold">{{ $exam->allow_retake ? '๊ฐ€๋Šฅ' : '๋ถˆ๊ฐ€' }}</p>
                    </div>
                </div>

                @if($userAttempts->isNotEmpty())
                    <div class="bg-yellow-50 border border-yellow-200 rounded-md p-4 mb-6">
                        <h3 class="font-bold mb-2">๐Ÿ“Š ์‘์‹œ ๊ธฐ๋ก</h3>
                        @foreach($userAttempts as $attempt)
                            <p class="text-sm">{{ $attempt->created_at->format('Y-m-d H:i') }} - ์ ์ˆ˜: {{ $attempt->score }}์  ({{ $attempt->passed ? 'โœ… ํ•ฉ๊ฒฉ' : 'โŒ ๋ถˆํ•ฉ๊ฒฉ' }})</p>
                        @endforeach
                    </div>
                @endif

                @auth
                    <a href="{{ route('exams.start', $exam) }}" class="block bg-blue-600 text-white text-center py-3 rounded-md hover:bg-blue-700 font-bold">
                        ๐Ÿš€ ์‹œํ—˜ ์‹œ์ž‘ํ•˜๊ธฐ
                    </a>
                @else
                    <p class="text-center text-gray-500">๋กœ๊ทธ์ธ ํ›„ ์‹œํ—˜์„ ์‘์‹œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.</p>
                    <a href="{{ route('login') }}" class="block bg-gray-600 text-white text-center py-3 rounded-md hover:bg-gray-700 mt-4">
                        ๋กœ๊ทธ์ธ
                    </a>
                @endauth
            </div>
        </div>
    </div>
</x-app-layout>
```

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ํ™ˆ ํ™”๋ฉด์—์„œ ๊ณต๊ฐœ๋œ ์‹œํ—˜ ๋ชฉ๋ก์„ ์นด๋“œ ํ˜•ํƒœ๋กœ ํ‘œ์‹œ. ์นดํ…Œ๊ณ ๋ฆฌ/๋‚œ์ด๋„ ํ•„ํ„ฐ ์ž‘๋™. ์‹œํ—˜ ์ƒ์„ธ ํŽ˜์ด์ง€์—์„œ ์‹œํ—˜ ์ •๋ณด, ์‘์‹œ ๊ธฐ๋ก ํ‘œ์‹œ. '์‹œํ—˜ ์‹œ์ž‘ํ•˜๊ธฐ' ๋ฒ„ํŠผ ํ‘œ์‹œ (๋กœ๊ทธ์ธ ํ•„์š”).

5
Step 5
์‹œํ—˜ ์‘์‹œ ํŽ˜์ด์ง€ ๊ตฌํ˜„ (์‹ค์‹œ๊ฐ„ ํƒ€์ด๋จธ ํฌํ•จ):

1. **ExamController์— ์‘์‹œ ๊ด€๋ จ ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€**:
```php
public function start(Exam $exam)
{
    if ($exam->status !== 'published') {
        abort(403);
    }

    // ์žฌ์‘์‹œ ๋ถˆ๊ฐ€ ์‹œํ—˜์ธ ๊ฒฝ์šฐ ๊ธฐ์กด ์‘์‹œ ๊ธฐ๋ก ํ™•์ธ
    if (!$exam->allow_retake) {
        $existingAttempt = auth()->user()->examAttempts()
            ->where('exam_id', $exam->id)
            ->whereNotNull('submitted_at')
            ->first();

        if ($existingAttempt) {
            return redirect()->route('exams.result', $existingAttempt->id)
                ->with('error', '์ด๋ฏธ ์‘์‹œํ•œ ์‹œํ—˜์ž…๋‹ˆ๋‹ค.');
        }
    }

    // ์ƒˆ ์‘์‹œ ๊ธฐ๋ก ์ƒ์„ฑ
    $attempt = auth()->user()->examAttempts()->create([
        'exam_id' => $exam->id,
        'started_at' => now(),
    ]);

    return redirect()->route('exams.take', $attempt->id);
}

public function take(ExamAttempt $attempt)
{
    if ($attempt->user_id !== auth()->id()) {
        abort(403);
    }

    if ($attempt->submitted_at) {
        return redirect()->route('exams.result', $attempt->id);
    }

    $exam = $attempt->exam->load('questions');
    $endTime = $attempt->started_at->addMinutes($exam->duration_minutes);

    return view('exams.take', compact('attempt', 'exam', 'endTime'));
}

public function saveAnswer(Request $request, ExamAttempt $attempt)
{
    if ($attempt->user_id !== auth()->id() || $attempt->submitted_at) {
        return response()->json(['error' => 'Unauthorized'], 403);
    }

    $answers = $attempt->answers ?? [];
    $answers[$request->question_id] = $request->answer;
    $attempt->update(['answers' => $answers]);

    return response()->json(['success' => true]);
}

public function submit(ExamAttempt $attempt)
{
    if ($attempt->user_id !== auth()->id() || $attempt->submitted_at) {
        abort(403);
    }

    $exam = $attempt->exam->load('questions');
    $answers = $attempt->answers ?? [];
    $score = 0;

    // ์ž๋™ ์ฑ„์  (๊ฐ๊ด€์‹๋งŒ)
    foreach ($exam->questions as $question) {
        $userAnswer = $answers[$question->id] ?? null;
        if ($question->question_type === 'multiple_choice' && $userAnswer === $question->correct_answer) {
            $score += $question->points;
        }
    }

    $attempt->update([
        'submitted_at' => now(),
        'score' => $score,
        'passed' => $score >= $exam->passing_score,
    ]);

    return redirect()->route('exams.result', $attempt->id);
}
```

2. **๋ผ์šฐํŠธ ์ถ”๊ฐ€**:
```php
Route::middleware('auth')->group(function () {
    Route::post('/exams/{exam}/start', [ExamController::class, 'start'])->name('exams.start');
    Route::get('/attempts/{attempt}', [ExamController::class, 'take'])->name('exams.take');
    Route::post('/attempts/{attempt}/answer', [ExamController::class, 'saveAnswer'])->name('exams.saveAnswer');
    Route::post('/attempts/{attempt}/submit', [ExamController::class, 'submit'])->name('exams.submit');
});
```

3. **Blade ๋ทฐ ์ƒ์„ฑ** (`resources/views/exams/take.blade.php`):
```blade
<x-app-layout>
    <div class="py-12" x-data="examTimer(@js($endTime))">
        <div class="max-w-5xl mx-auto sm:px-6 lg:px-8">
            <!-- ํƒ€์ด๋จธ & ์ง„ํ–‰๋ฅ  -->
            <div class="bg-white rounded-lg shadow-md p-4 mb-6 flex justify-between items-center">
                <div>
                    <h2 class="text-xl font-bold">{{ $exam->title }}</h2>
                    <p class="text-sm text-gray-500">๋ฌธ์ œ <span x-text="currentQuestion">1</span> / {{ $exam->questions->count() }}</p>
                </div>
                <div class="text-right">
                    <p class="text-sm text-gray-500">๋‚จ์€ ์‹œ๊ฐ„</p>
                    <p class="text-2xl font-bold" :class="timeLeft < 300 ? 'text-red-600' : 'text-green-600'" x-text="formatTime(timeLeft)"></p>
                </div>
            </div>

            <!-- ๋ฌธ์ œ -->
            <div class="bg-white rounded-lg shadow-md p-8">
                @foreach($exam->questions as $index => $question)
                    <div class="mb-8" x-show="currentQuestion === {{ $index + 1 }}">
                        <h3 class="text-lg font-bold mb-4">{{ $index + 1 }}. {{ $question->question_text }}</h3>

                        @if($question->question_type === 'multiple_choice')
                            <div class="space-y-3">
                                @foreach(json_decode($question->options) as $option)
                                    <label class="flex items-center p-3 border rounded-md hover:bg-gray-50 cursor-pointer">
                                        <input type="radio" name="question_{{ $question->id }}" value="{{ $option }}" class="mr-3" @change="saveAnswer({{ $question->id }}, $event.target.value)">
                                        <span>{{ $option }}</span>
                                    </label>
                                @endforeach
                            </div>
                        @else
                            <input type="text" class="w-full border-gray-300 rounded-md" placeholder="๋‹ต์„ ์ž…๋ ฅํ•˜์„ธ์š”" @input="saveAnswer({{ $question->id }}, $event.target.value)">
                        @endif
                    </div>
                @endforeach

                <!-- ๋„ค๋น„๊ฒŒ์ด์…˜ -->
                <div class="flex justify-between mt-8">
                    <button @click="currentQuestion > 1 && currentQuestion--" class="bg-gray-300 px-4 py-2 rounded-md" :disabled="currentQuestion === 1">์ด์ „</button>
                    <button @click="currentQuestion < {{ $exam->questions->count() }} && currentQuestion++" class="bg-blue-600 text-white px-4 py-2 rounded-md" :disabled="currentQuestion === {{ $exam->questions->count() }}">๋‹ค์Œ</button>
                </div>

                <!-- ์ œ์ถœ ๋ฒ„ํŠผ -->
                <form method="POST" action="{{ route('exams.submit', $attempt) }}" class="mt-8">
                    @csrf
                    <button type="submit" class="w-full bg-green-600 text-white py-3 rounded-md font-bold hover:bg-green-700">
                        ๐ŸŽฏ ์‹œํ—˜ ์ œ์ถœํ•˜๊ธฐ
                    </button>
                </form>
            </div>
        </div>
    </div>

    <script>
        function examTimer(endTime) {
            return {
                currentQuestion: 1,
                timeLeft: Math.floor((new Date(endTime) - new Date()) / 1000),
                interval: null,

                init() {
                    this.interval = setInterval(() => {
                        this.timeLeft--;
                        if (this.timeLeft <= 0) {
                            clearInterval(this.interval);
                            document.querySelector('form').submit(); // ์ž๋™ ์ œ์ถœ
                        }
                    }, 1000);
                },

                formatTime(seconds) {
                    const mins = Math.floor(seconds / 60);
                    const secs = seconds % 60;
                    return `${mins}:${secs.toString().padStart(2, '0')}`;
                },

                saveAnswer(questionId, answer) {
                    fetch(`/attempts/{{ $attempt->id }}/answer`, {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/json',
                            'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
                        },
                        body: JSON.stringify({ question_id: questionId, answer: answer })
                    });
                }
            }
        }
    </script>
</x-app-layout>
```

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ์‹œํ—˜ ์‘์‹œ ํŽ˜์ด์ง€ ๊ตฌํ˜„ ์™„๋ฃŒ. ์‹ค์‹œ๊ฐ„ ์นด์šดํŠธ๋‹ค์šด ํƒ€์ด๋จธ ์ž‘๋™ (์‹œ๊ฐ„ ์ดˆ๊ณผ ์‹œ ์ž๋™ ์ œ์ถœ). ๋ฌธ์ œ ๋„ค๋น„๊ฒŒ์ด์…˜ (์ด์ „/๋‹ค์Œ ๋ฒ„ํŠผ). ๋‹ต์•ˆ ์„ ํƒ ์‹œ AJAX๋กœ ์ž๋™ ์ €์žฅ. ์ œ์ถœ ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ์ฑ„์  ๋ฐ ๊ฒฐ๊ณผ ํŽ˜์ด์ง€๋กœ ์ด๋™.

6
Step 6
๊ฒฐ๊ณผ ํŽ˜์ด์ง€ ๋ฐ ๋งˆ์ดํŽ˜์ด์ง€ ๊ตฌํ˜„:

1. **ExamController์— ๊ฒฐ๊ณผ ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€**:
```php
public function result(ExamAttempt $attempt)
{
    if ($attempt->user_id !== auth()->id()) {
        abort(403);
    }

    if (!$attempt->submitted_at) {
        return redirect()->route('exams.take', $attempt->id);
    }

    $exam = $attempt->exam->load('questions');
    $answers = $attempt->answers ?? [];

    // ๋ฌธ์ œ๋ณ„ ์ •์˜ค๋‹ต ๋ถ„์„
    $results = $exam->questions->map(function ($question) use ($answers) {
        $userAnswer = $answers[$question->id] ?? null;
        return [
            'question' => $question,
            'user_answer' => $userAnswer,
            'is_correct' => $question->question_type === 'multiple_choice' && $userAnswer === $question->correct_answer,
        ];
    });

    return view('exams.result', compact('attempt', 'exam', 'results'));
}
```

2. **๋ผ์šฐํŠธ ์ถ”๊ฐ€**:
```php
Route::middleware('auth')->group(function () {
    Route::get('/attempts/{attempt}/result', [ExamController::class, 'result'])->name('exams.result');
    Route::get('/my-exams', [ExamController::class, 'myExams'])->name('exams.my');
});
```

3. **Blade ๋ทฐ** (`resources/views/exams/result.blade.php`):
```blade
<x-app-layout>
    <div class="py-12">
        <div class="max-w-4xl mx-auto sm:px-6 lg:px-8">
            <!-- ๊ฒฐ๊ณผ ์š”์•ฝ -->
            <div class="bg-white rounded-lg shadow-md p-8 mb-6 text-center">
                @if($attempt->passed)
                    <div class="text-6xl mb-4">๐ŸŽ‰</div>
                    <h1 class="text-3xl font-bold text-green-600 mb-2">ํ•ฉ๊ฒฉ์„ ์ถ•ํ•˜ํ•ฉ๋‹ˆ๋‹ค!</h1>
                @else
                    <div class="text-6xl mb-4">๐Ÿ˜ข</div>
                    <h1 class="text-3xl font-bold text-red-600 mb-2">์•„์‰ฝ๊ฒŒ๋„ ๋ถˆํ•ฉ๊ฒฉ์ž…๋‹ˆ๋‹ค</h1>
                @endif

                <div class="grid grid-cols-3 gap-4 mt-8">
                    <div class="border-l-4 border-blue-500 pl-4">
                        <p class="text-sm text-gray-500">์ทจ๋“ ์ ์ˆ˜</p>
                        <p class="text-3xl font-bold">{{ $attempt->score }}์ </p>
                    </div>
                    <div class="border-l-4 border-green-500 pl-4">
                        <p class="text-sm text-gray-500">ํ•ฉ๊ฒฉ ์ ์ˆ˜</p>
                        <p class="text-3xl font-bold">{{ $exam->passing_score }}์ </p>
                    </div>
                    <div class="border-l-4 border-purple-500 pl-4">
                        <p class="text-sm text-gray-500">์ •๋‹ต๋ฅ </p>
                        <p class="text-3xl font-bold">{{ round(($results->where('is_correct', true)->count() / $results->count()) * 100) }}%</p>
                    </div>
                </div>
            </div>

            <!-- ๋ฌธ์ œ๋ณ„ ์ƒ์„ธ ๊ฒฐ๊ณผ -->
            <div class="bg-white rounded-lg shadow-md p-8">
                <h2 class="text-2xl font-bold mb-6">๐Ÿ“ ๋ฌธ์ œ๋ณ„ ์ƒ์„ธ ๊ฒฐ๊ณผ</h2>

                @foreach($results as $index => $result)
                    <div class="mb-6 pb-6 border-b">
                        <div class="flex items-start justify-between">
                            <h3 class="text-lg font-bold mb-2">{{ $index + 1 }}. {{ $result['question']->question_text }}</h3>
                            @if($result['is_correct'])
                                <span class="bg-green-100 text-green-800 text-xs px-2 py-1 rounded">โœ… ์ •๋‹ต</span>
                            @else
                                <span class="bg-red-100 text-red-800 text-xs px-2 py-1 rounded">โŒ ์˜ค๋‹ต</span>
                            @endif
                        </div>

                        <p class="text-sm text-gray-600 mt-2">๋‹น์‹ ์˜ ๋‹ต: <strong>{{ $result['user_answer'] ?? '๋ฏธ์‘๋‹ต' }}</strong></p>
                        @if(!$result['is_correct'])
                            <p class="text-sm text-green-600 mt-1">์ •๋‹ต: <strong>{{ $result['question']->correct_answer }}</strong></p>
                        @endif
                    </div>
                @endforeach
            </div>

            <div class="mt-6 flex gap-4">
                <a href="{{ route('home') }}" class="flex-1 bg-gray-600 text-white text-center py-3 rounded-md hover:bg-gray-700">
                    ๐Ÿ  ํ™ˆ์œผ๋กœ
                </a>
                @if($exam->allow_retake)
                    <a href="{{ route('exams.show', $exam) }}" class="flex-1 bg-blue-600 text-white text-center py-3 rounded-md hover:bg-blue-700">
                        ๐Ÿ”„ ๋‹ค์‹œ ๋„์ „ํ•˜๊ธฐ
                    </a>
                @endif
            </div>
        </div>
    </div>
</x-app-layout>
```

4. **๋งˆ์ดํŽ˜์ด์ง€ ๋ทฐ** (`resources/views/exams/my.blade.php`):
```blade
<x-app-layout>
    <div class="py-12">
        <div class="max-w-5xl mx-auto sm:px-6 lg:px-8">
            <h1 class="text-3xl font-bold mb-8">๐Ÿ“Š ๋‚˜์˜ ์‘์‹œ ๊ธฐ๋ก</h1>

            <div class="bg-white rounded-lg shadow-md overflow-hidden">
                <table class="w-full">
                    <thead class="bg-gray-100">
                        <tr>
                            <th class="px-6 py-3 text-left">์‹œํ—˜๋ช…</th>
                            <th class="px-6 py-3 text-left">์‘์‹œ์ผ</th>
                            <th class="px-6 py-3 text-left">์ ์ˆ˜</th>
                            <th class="px-6 py-3 text-left">๊ฒฐ๊ณผ</th>
                            <th class="px-6 py-3 text-left">์ƒ์„ธ</th>
                        </tr>
                    </thead>
                    <tbody>
                        @forelse(auth()->user()->examAttempts()->with('exam')->latest()->get() as $attempt)
                            <tr class="border-b">
                                <td class="px-6 py-4">{{ $attempt->exam->title }}</td>
                                <td class="px-6 py-4">{{ $attempt->created_at->format('Y-m-d H:i') }}</td>
                                <td class="px-6 py-4 font-bold">{{ $attempt->score }}์ </td>
                                <td class="px-6 py-4">
                                    @if($attempt->passed)
                                        <span class="bg-green-100 text-green-800 text-xs px-2 py-1 rounded">โœ… ํ•ฉ๊ฒฉ</span>
                                    @else
                                        <span class="bg-red-100 text-red-800 text-xs px-2 py-1 rounded">โŒ ๋ถˆํ•ฉ๊ฒฉ</span>
                                    @endif
                                </td>
                                <td class="px-6 py-4">
                                    <a href="{{ route('exams.result', $attempt) }}" class="text-blue-600 hover:underline">๊ฒฐ๊ณผ ๋ณด๊ธฐ</a>
                                </td>
                            </tr>
                        @empty
                            <tr>
                                <td colspan="5" class="px-6 py-8 text-center text-gray-500">์•„์ง ์‘์‹œํ•œ ์‹œํ—˜์ด ์—†์Šต๋‹ˆ๋‹ค.</td>
                            </tr>
                        @endforelse
                    </tbody>
                </table>
            </div>
        </div>
    </div>
</x-app-layout>
```

5. **๋„ค๋น„๊ฒŒ์ด์…˜์— ๋งˆ์ดํŽ˜์ด์ง€ ๋งํฌ ์ถ”๊ฐ€** (`resources/views/layouts/navigation.blade.php`):
```blade
<x-nav-link :href="route('exams.my')" :active="request()->routeIs('exams.my')">
    ๋‚˜์˜ ์‹œํ—˜ ๊ธฐ๋ก
</x-nav-link>
```

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ๊ฒฐ๊ณผ ํŽ˜์ด์ง€์—์„œ ํ•ฉ๊ฒฉ/๋ถˆํ•ฉ๊ฒฉ ์—ฌ๋ถ€, ์ด์ , ์ •๋‹ต๋ฅ  ํ‘œ์‹œ. ๋ฌธ์ œ๋ณ„ ์ •์˜ค๋‹ต ์ƒ์„ธ ๋ถ„์„ ํ‘œ์‹œ. ๋งˆ์ดํŽ˜์ด์ง€์—์„œ ์‘์‹œ ๊ธฐ๋ก ํ…Œ์ด๋ธ” ํ˜•ํƒœ๋กœ ํ™•์ธ ๊ฐ€๋Šฅ. ๋„ค๋น„๊ฒŒ์ด์…˜์— ๋งˆ์ดํŽ˜์ด์ง€ ๋งํฌ ์ถ”๊ฐ€.

7
Step 7
๊ด€๋ฆฌ์ž ํŒจ๋„ ๊ตฌํ˜„ (์‹œํ—˜/๋ฌธ์ œ CRUD):

1. **Policy ์ƒ์„ฑ** (๊ด€๋ฆฌ์ž๋งŒ ์ ‘๊ทผ):
```bash
php artisan make:policy ExamPolicy --model=Exam
```

`app/Policies/ExamPolicy.php`:
```php
public function viewAny(User $user)
{
    return $user->role === 'admin';
}

public function create(User $user)
{
    return $user->role === 'admin';
}

public function update(User $user, Exam $exam)
{
    return $user->role === 'admin';
}

public function delete(User $user, Exam $exam)
{
    return $user->role === 'admin';
}
```

2. **AdminExamController ์ƒ์„ฑ**:
```bash
php artisan make:controller Admin/ExamController --resource
```

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

use App\Http\Controllers\Controller;
use App\Models\Exam;
use Illuminate\Http\Request;

public function __construct()
{
    $this->authorizeResource(Exam::class, 'exam');
}

public function index()
{
    $exams = Exam::withCount('questions', 'attempts')->latest()->paginate(10);
    return view('admin.exams.index', compact('exams'));
}

public function create()
{
    return view('admin.exams.create');
}

public function store(Request $request)
{
    $validated = $request->validate([
        'title' => 'required|max:255',
        'description' => 'nullable',
        'duration_minutes' => 'required|integer|min:1',
        'passing_score' => 'required|integer|min:0|max:100',
        'category' => 'required|max:100',
        'difficulty' => 'required|in:beginner,intermediate,advanced',
        'status' => 'required|in:draft,published',
        'allow_retake' => 'boolean',
    ]);

    Exam::create($validated);

    return redirect()->route('admin.exams.index')->with('success', '์‹œํ—˜์ด ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
}

public function edit(Exam $exam)
{
    return view('admin.exams.edit', compact('exam'));
}

public function update(Request $request, Exam $exam)
{
    $validated = $request->validate([
        'title' => 'required|max:255',
        'description' => 'nullable',
        'duration_minutes' => 'required|integer|min:1',
        'passing_score' => 'required|integer|min:0|max:100',
        'category' => 'required|max:100',
        'difficulty' => 'required|in:beginner,intermediate,advanced',
        'status' => 'required|in:draft,published',
        'allow_retake' => 'boolean',
    ]);

    $exam->update($validated);

    return redirect()->route('admin.exams.index')->with('success', '์‹œํ—˜์ด ์ˆ˜์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
}

public function destroy(Exam $exam)
{
    $exam->delete();
    return redirect()->route('admin.exams.index')->with('success', '์‹œํ—˜์ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
}
```

3. **๋ผ์šฐํŠธ ์„ค์ •** (`routes/web.php`):
```php
use App\Http\Controllers\Admin\ExamController as AdminExamController;

Route::middleware(['auth'])->prefix('admin')->name('admin.')->group(function () {
    Route::resource('exams', AdminExamController::class);
});
```

4. **Blade ๋ทฐ** (`resources/views/admin/exams/index.blade.php`):
```blade
<x-app-layout>
    <div class="py-12">
        <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
            <div class="flex justify-between items-center mb-8">
                <h1 class="text-3xl font-bold">๐ŸŽ“ ์‹œํ—˜ ๊ด€๋ฆฌ</h1>
                <a href="{{ route('admin.exams.create') }}" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700">
                    + ์ƒˆ ์‹œํ—˜ ๋งŒ๋“ค๊ธฐ
                </a>
            </div>

            @if(session('success'))
                <div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4">
                    {{ session('success') }}
                </div>
            @endif

            <div class="bg-white rounded-lg shadow-md overflow-hidden">
                <table class="w-full">
                    <thead class="bg-gray-100">
                        <tr>
                            <th class="px-6 py-3 text-left">์ œ๋ชฉ</th>
                            <th class="px-6 py-3 text-left">์นดํ…Œ๊ณ ๋ฆฌ</th>
                            <th class="px-6 py-3 text-left">๋ฌธ์ œ ์ˆ˜</th>
                            <th class="px-6 py-3 text-left">์‘์‹œ์ž ์ˆ˜</th>
                            <th class="px-6 py-3 text-left">์ƒํƒœ</th>
                            <th class="px-6 py-3 text-left">๊ด€๋ฆฌ</th>
                        </tr>
                    </thead>
                    <tbody>
                        @foreach($exams as $exam)
                            <tr class="border-b">
                                <td class="px-6 py-4 font-bold">{{ $exam->title }}</td>
                                <td class="px-6 py-4">{{ $exam->category }}</td>
                                <td class="px-6 py-4">{{ $exam->questions_count }}๋ฌธ์ œ</td>
                                <td class="px-6 py-4">{{ $exam->attempts_count }}๋ช…</td>
                                <td class="px-6 py-4">
                                    @if($exam->status === 'published')
                                        <span class="bg-green-100 text-green-800 text-xs px-2 py-1 rounded">๊ณต๊ฐœ</span>
                                    @else
                                        <span class="bg-gray-100 text-gray-800 text-xs px-2 py-1 rounded">๋น„๊ณต๊ฐœ</span>
                                    @endif
                                </td>
                                <td class="px-6 py-4 flex gap-2">
                                    <a href="{{ route('admin.exams.edit', $exam) }}" class="text-blue-600 hover:underline">์ˆ˜์ •</a>
                                    <form method="POST" action="{{ route('admin.exams.destroy', $exam) }}" onsubmit="return confirm('์ •๋ง ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?')">
                                        @csrf
                                        @method('DELETE')
                                        <button type="submit" class="text-red-600 hover:underline">์‚ญ์ œ</button>
                                    </form>
                                </td>
                            </tr>
                        @endforeach
                    </tbody>
                </table>
            </div>

            <div class="mt-6">
                {{ $exams->links() }}
            </div>
        </div>
    </div>
</x-app-layout>
```

5. **์‹œํ—˜ ์ƒ์„ฑ/์ˆ˜์ • ํผ** (`resources/views/admin/exams/create.blade.php`, `edit.blade.php`):
```blade
<x-app-layout>
    <div class="py-12">
        <div class="max-w-3xl mx-auto sm:px-6 lg:px-8">
            <h1 class="text-3xl font-bold mb-8">{{ isset($exam) ? '์‹œํ—˜ ์ˆ˜์ •' : '์ƒˆ ์‹œํ—˜ ๋งŒ๋“ค๊ธฐ' }}</h1>

            <form method="POST" action="{{ isset($exam) ? route('admin.exams.update', $exam) : route('admin.exams.store') }}" class="bg-white rounded-lg shadow-md p-8">
                @csrf
                @if(isset($exam))
                    @method('PUT')
                @endif

                <div class="mb-6">
                    <label class="block text-sm font-bold mb-2">์‹œํ—˜ ์ œ๋ชฉ *</label>
                    <input type="text" name="title" value="{{ old('title', $exam->title ?? '') }}" class="w-full border-gray-300 rounded-md" required>
                    @error('title')<p class="text-red-600 text-sm mt-1">{{ $message }}</p>@enderror
                </div>

                <div class="mb-6">
                    <label class="block text-sm font-bold mb-2">์„ค๋ช…</label>
                    <textarea name="description" rows="4" class="w-full border-gray-300 rounded-md">{{ old('description', $exam->description ?? '') }}</textarea>
                </div>

                <div class="grid grid-cols-2 gap-4 mb-6">
                    <div>
                        <label class="block text-sm font-bold mb-2">์ œํ•œ ์‹œ๊ฐ„ (๋ถ„) *</label>
                        <input type="number" name="duration_minutes" value="{{ old('duration_minutes', $exam->duration_minutes ?? 30) }}" class="w-full border-gray-300 rounded-md" required>
                    </div>
                    <div>
                        <label class="block text-sm font-bold mb-2">ํ•ฉ๊ฒฉ ์ ์ˆ˜ *</label>
                        <input type="number" name="passing_score" value="{{ old('passing_score', $exam->passing_score ?? 70) }}" class="w-full border-gray-300 rounded-md" required>
                    </div>
                </div>

                <div class="grid grid-cols-2 gap-4 mb-6">
                    <div>
                        <label class="block text-sm font-bold mb-2">์นดํ…Œ๊ณ ๋ฆฌ *</label>
                        <input type="text" name="category" value="{{ old('category', $exam->category ?? '') }}" class="w-full border-gray-300 rounded-md" required>
                    </div>
                    <div>
                        <label class="block text-sm font-bold mb-2">๋‚œ์ด๋„ *</label>
                        <select name="difficulty" class="w-full border-gray-300 rounded-md" required>
                            <option value="beginner" {{ old('difficulty', $exam->difficulty ?? '') === 'beginner' ? 'selected' : '' }}>์ดˆ๊ธ‰</option>
                            <option value="intermediate" {{ old('difficulty', $exam->difficulty ?? '') === 'intermediate' ? 'selected' : '' }}>์ค‘๊ธ‰</option>
                            <option value="advanced" {{ old('difficulty', $exam->difficulty ?? '') === 'advanced' ? 'selected' : '' }}>๊ณ ๊ธ‰</option>
                        </select>
                    </div>
                </div>

                <div class="mb-6">
                    <label class="block text-sm font-bold mb-2">๊ณต๊ฐœ ์ƒํƒœ *</label>
                    <select name="status" class="w-full border-gray-300 rounded-md" required>
                        <option value="draft" {{ old('status', $exam->status ?? '') === 'draft' ? 'selected' : '' }}>๋น„๊ณต๊ฐœ</option>
                        <option value="published" {{ old('status', $exam->status ?? '') === 'published' ? 'selected' : '' }}>๊ณต๊ฐœ</option>
                    </select>
                </div>

                <div class="mb-6">
                    <label class="flex items-center">
                        <input type="checkbox" name="allow_retake" value="1" {{ old('allow_retake', $exam->allow_retake ?? true) ? 'checked' : '' }} class="mr-2">
                        <span class="text-sm">์žฌ์‘์‹œ ํ—ˆ์šฉ</span>
                    </label>
                </div>

                <button type="submit" class="w-full bg-blue-600 text-white py-3 rounded-md font-bold hover:bg-blue-700">
                    {{ isset($exam) ? '์ˆ˜์ •ํ•˜๊ธฐ' : '์ƒ์„ฑํ•˜๊ธฐ' }}
                </button>
            </form>
        </div>
    </div>
</x-app-layout>
```

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ๊ด€๋ฆฌ์ž ์ „์šฉ ์‹œํ—˜ ๊ด€๋ฆฌ ํŽ˜์ด์ง€ ๊ตฌํ˜„ ์™„๋ฃŒ. Policy๋กœ ๊ถŒํ•œ ์ œ์–ด (๊ด€๋ฆฌ์ž๋งŒ ์ ‘๊ทผ). ์‹œํ—˜ ๋ชฉ๋ก, ์ƒ์„ฑ, ์ˆ˜์ •, ์‚ญ์ œ CRUD ๊ธฐ๋Šฅ ์ž‘๋™. ์‹œํ—˜ ์ •๋ณด ํผ ์œ ํšจ์„ฑ ๊ฒ€์ฆ ์ ์šฉ.

8
Step 8
๋ฌธ์ œ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ ๊ตฌํ˜„:

1. **QuestionController ์ƒ์„ฑ**:
```bash
php artisan make:controller Admin/QuestionController --resource
```

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

use App\Http\Controllers\Controller;
use App\Models\Exam;
use App\Models\Question;
use Illuminate\Http\Request;

public function index(Exam $exam)
{
    $this->authorize('update', $exam);
    $questions = $exam->questions()->orderBy('order')->get();
    return view('admin.questions.index', compact('exam', 'questions'));
}

public function create(Exam $exam)
{
    $this->authorize('update', $exam);
    return view('admin.questions.create', compact('exam'));
}

public function store(Request $request, Exam $exam)
{
    $this->authorize('update', $exam);

    $validated = $request->validate([
        'question_text' => 'required',
        'question_type' => 'required|in:multiple_choice,short_answer',
        'options' => 'required_if:question_type,multiple_choice|array|min:2',
        'options.*' => 'required_if:question_type,multiple_choice|string',
        'correct_answer' => 'required',
        'points' => 'required|integer|min:1',
    ]);

    $validated['exam_id'] = $exam->id;
    $validated['order'] = $exam->questions()->max('order') + 1;
    $validated['options'] = $request->question_type === 'multiple_choice' ? json_encode($request->options) : null;

    Question::create($validated);

    return redirect()->route('admin.questions.index', $exam)->with('success', '๋ฌธ์ œ๊ฐ€ ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
}

public function edit(Exam $exam, Question $question)
{
    $this->authorize('update', $exam);
    return view('admin.questions.edit', compact('exam', 'question'));
}

public function update(Request $request, Exam $exam, Question $question)
{
    $this->authorize('update', $exam);

    $validated = $request->validate([
        'question_text' => 'required',
        'question_type' => 'required|in:multiple_choice,short_answer',
        'options' => 'required_if:question_type,multiple_choice|array|min:2',
        'options.*' => 'required_if:question_type,multiple_choice|string',
        'correct_answer' => 'required',
        'points' => 'required|integer|min:1',
    ]);

    $validated['options'] = $request->question_type === 'multiple_choice' ? json_encode($request->options) : null;
    $question->update($validated);

    return redirect()->route('admin.questions.index', $exam)->with('success', '๋ฌธ์ œ๊ฐ€ ์ˆ˜์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
}

public function destroy(Exam $exam, Question $question)
{
    $this->authorize('update', $exam);
    $question->delete();
    return redirect()->route('admin.questions.index', $exam)->with('success', '๋ฌธ์ œ๊ฐ€ ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
}
```

2. **๋ผ์šฐํŠธ ์ถ”๊ฐ€**:
```php
Route::middleware(['auth'])->prefix('admin')->name('admin.')->group(function () {
    Route::resource('exams', AdminExamController::class);
    Route::resource('exams.questions', QuestionController::class)->except(['show']);
});
```

3. **๋ฌธ์ œ ๋ชฉ๋ก ๋ทฐ** (`resources/views/admin/questions/index.blade.php`):
```blade
<x-app-layout>
    <div class="py-12">
        <div class="max-w-5xl mx-auto sm:px-6 lg:px-8">
            <div class="flex justify-between items-center mb-8">
                <div>
                    <h1 class="text-3xl font-bold">๐Ÿ“ ๋ฌธ์ œ ๊ด€๋ฆฌ</h1>
                    <p class="text-gray-600 mt-2">์‹œํ—˜: {{ $exam->title }}</p>
                </div>
                <div class="flex gap-2">
                    <a href="{{ route('admin.exams.index') }}" class="bg-gray-600 text-white px-4 py-2 rounded-md hover:bg-gray-700">โ† ์‹œํ—˜ ๋ชฉ๋ก</a>
                    <a href="{{ route('admin.questions.create', $exam) }}" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700">+ ๋ฌธ์ œ ์ถ”๊ฐ€</a>
                </div>
            </div>

            @if(session('success'))
                <div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4">
                    {{ session('success') }}
                </div>
            @endif

            <div class="space-y-4">
                @forelse($questions as $question)
                    <div class="bg-white rounded-lg shadow-md p-6">
                        <div class="flex justify-between items-start">
                            <div class="flex-1">
                                <h3 class="text-lg font-bold mb-2">{{ $question->order }}. {{ $question->question_text }}</h3>
                                @if($question->question_type === 'multiple_choice')
                                    <div class="space-y-1 text-sm text-gray-600">
                                        @foreach(json_decode($question->options) as $option)
                                            <p class="{{ $option === $question->correct_answer ? 'text-green-600 font-bold' : '' }}">
                                                {{ $option === $question->correct_answer ? 'โœ…' : 'โ—‹' }} {{ $option }}
                                            </p>
                                        @endforeach
                                    </div>
                                @else
                                    <p class="text-sm text-green-600">์ •๋‹ต: {{ $question->correct_answer }}</p>
                                @endif
                                <p class="text-xs text-gray-500 mt-2">๋ฐฐ์ : {{ $question->points }}์ </p>
                            </div>
                            <div class="flex gap-2">
                                <a href="{{ route('admin.questions.edit', [$exam, $question]) }}" class="text-blue-600 hover:underline text-sm">์ˆ˜์ •</a>
                                <form method="POST" action="{{ route('admin.questions.destroy', [$exam, $question]) }}" onsubmit="return confirm('์ •๋ง ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?')">
                                    @csrf
                                    @method('DELETE')
                                    <button type="submit" class="text-red-600 hover:underline text-sm">์‚ญ์ œ</button>
                                </form>
                            </div>
                        </div>
                    </div>
                @empty
                    <div class="bg-white rounded-lg shadow-md p-8 text-center text-gray-500">
                        ์•„์ง ๋“ฑ๋ก๋œ ๋ฌธ์ œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ์ฒซ ๋ฒˆ์งธ ๋ฌธ์ œ๋ฅผ ์ถ”๊ฐ€ํ•ด๋ณด์„ธ์š”!
                    </div>
                @endforelse
            </div>
        </div>
    </div>
</x-app-layout>
```

4. **๋ฌธ์ œ ์ƒ์„ฑ/์ˆ˜์ • ํผ** (`resources/views/admin/questions/create.blade.php`, `edit.blade.php`):
```blade
<x-app-layout>
    <div class="py-12">
        <div class="max-w-3xl mx-auto sm:px-6 lg:px-8">
            <h1 class="text-3xl font-bold mb-8">{{ isset($question) ? '๋ฌธ์ œ ์ˆ˜์ •' : '์ƒˆ ๋ฌธ์ œ ์ถ”๊ฐ€' }}</h1>

            <form method="POST" action="{{ isset($question) ? route('admin.questions.update', [$exam, $question]) : route('admin.questions.store', $exam) }}" class="bg-white rounded-lg shadow-md p-8" x-data="questionForm()">
                @csrf
                @if(isset($question))
                    @method('PUT')
                @endif

                <div class="mb-6">
                    <label class="block text-sm font-bold mb-2">๋ฌธ์ œ ์œ ํ˜• *</label>
                    <select name="question_type" x-model="questionType" class="w-full border-gray-300 rounded-md" required>
                        <option value="multiple_choice">๊ฐ๊ด€์‹</option>
                        <option value="short_answer">์ฃผ๊ด€์‹</option>
                    </select>
                </div>

                <div class="mb-6">
                    <label class="block text-sm font-bold mb-2">๋ฌธ์ œ ๋‚ด์šฉ *</label>
                    <textarea name="question_text" rows="4" class="w-full border-gray-300 rounded-md" required>{{ old('question_text', $question->question_text ?? '') }}</textarea>
                </div>

                <div class="mb-6" x-show="questionType === 'multiple_choice'">
                    <label class="block text-sm font-bold mb-2">์„ ํƒ์ง€ (์ตœ์†Œ 2๊ฐœ) *</label>
                    <template x-for="(option, index) in options" :key="index">
                        <div class="flex gap-2 mb-2">
                            <input type="text" :name="'options[' + index + ']'" x-model="options[index]" class="flex-1 border-gray-300 rounded-md" placeholder="์„ ํƒ์ง€ ์ž…๋ ฅ">
                            <button type="button" @click="options.splice(index, 1)" class="bg-red-600 text-white px-3 py-2 rounded-md" x-show="options.length > 2">์‚ญ์ œ</button>
                        </div>
                    </template>
                    <button type="button" @click="options.push('')" class="bg-gray-600 text-white px-4 py-2 rounded-md mt-2">+ ์„ ํƒ์ง€ ์ถ”๊ฐ€</button>
                </div>

                <div class="mb-6">
                    <label class="block text-sm font-bold mb-2">์ •๋‹ต *</label>
                    <input type="text" name="correct_answer" value="{{ old('correct_answer', $question->correct_answer ?? '') }}" class="w-full border-gray-300 rounded-md" required>
                    <p class="text-xs text-gray-500 mt-1">๊ฐ๊ด€์‹์ธ ๊ฒฝ์šฐ ์œ„ ์„ ํƒ์ง€ ์ค‘ ํ•˜๋‚˜๋ฅผ ์ •ํ™•ํžˆ ์ž…๋ ฅํ•˜์„ธ์š”.</p>
                </div>

                <div class="mb-6">
                    <label class="block text-sm font-bold mb-2">๋ฐฐ์  *</label>
                    <input type="number" name="points" value="{{ old('points', $question->points ?? 10) }}" class="w-full border-gray-300 rounded-md" required>
                </div>

                <button type="submit" class="w-full bg-blue-600 text-white py-3 rounded-md font-bold hover:bg-blue-700">
                    {{ isset($question) ? '์ˆ˜์ •ํ•˜๊ธฐ' : '์ถ”๊ฐ€ํ•˜๊ธฐ' }}
                </button>
            </form>
        </div>
    </div>

    <script>
        function questionForm() {
            return {
                questionType: '{{ old('question_type', $question->question_type ?? 'multiple_choice') }}',
                options: @json(old('options', isset($question) && $question->options ? json_decode($question->options) : ['', '']))
            }
        }
    </script>
</x-app-layout>
```

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ๋ฌธ์ œ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ ๊ตฌํ˜„ ์™„๋ฃŒ. ์‹œํ—˜๋ณ„ ๋ฌธ์ œ ๋ชฉ๋ก ์กฐํšŒ, ๋ฌธ์ œ ์ถ”๊ฐ€/์ˆ˜์ •/์‚ญ์ œ CRUD ๊ธฐ๋Šฅ ์ž‘๋™. ๊ฐ๊ด€์‹/์ฃผ๊ด€์‹ ๋ฌธ์ œ ์œ ํ˜• ์„ ํƒ ๊ฐ€๋Šฅ. Alpine.js๋กœ ๋™์  ์„ ํƒ์ง€ ์ถ”๊ฐ€/์‚ญ์ œ ๊ธฐ๋Šฅ ๊ตฌํ˜„.

9
Step 9
ํ†ต๊ณ„ ๋Œ€์‹œ๋ณด๋“œ ๋ฐ ๋ณด๋„ˆ์Šค ๊ธฐ๋Šฅ ๊ตฌํ˜„:

1. **ํ†ต๊ณ„ ๋Œ€์‹œ๋ณด๋“œ ์ถ”๊ฐ€** (`app/Http/Controllers/Admin/DashboardController.php`):
```bash
php artisan make:controller Admin/DashboardController
```

```php
namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Models\Exam;
use App\Models\ExamAttempt;
use Illuminate\Support\Facades\DB;

public function index()
{
    $this->authorize('viewAny', Exam::class);

    $stats = [
        'total_exams' => Exam::count(),
        'total_attempts' => ExamAttempt::count(),
        'pass_rate' => ExamAttempt::whereNotNull('submitted_at')->avg('passed') * 100,
        'avg_score' => ExamAttempt::whereNotNull('submitted_at')->avg('score'),
    ];

    $recentAttempts = ExamAttempt::with(['user', 'exam'])
        ->whereNotNull('submitted_at')
        ->latest()
        ->take(10)
        ->get();

    $examStats = Exam::withCount('attempts')
        ->withAvg('attempts', 'score')
        ->get()
        ->map(function ($exam) {
            return [
                'title' => $exam->title,
                'attempts' => $exam->attempts_count,
                'avg_score' => round($exam->attempts_avg_score ?? 0, 1),
            ];
        });

    return view('admin.dashboard', compact('stats', 'recentAttempts', 'examStats'));
}
```

2. **๋ผ์šฐํŠธ ์ถ”๊ฐ€**:
```php
Route::middleware(['auth'])->prefix('admin')->name('admin.')->group(function () {
    Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard');
    // ... ๊ธฐ์กด ๋ผ์šฐํŠธ
});
```

3. **๋Œ€์‹œ๋ณด๋“œ ๋ทฐ** (`resources/views/admin/dashboard.blade.php`):
```blade
<x-app-layout>
    <div class="py-12">
        <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
            <h1 class="text-3xl font-bold mb-8">๐Ÿ“Š ๊ด€๋ฆฌ์ž ๋Œ€์‹œ๋ณด๋“œ</h1>

            <!-- ํ†ต๊ณ„ ์นด๋“œ -->
            <div class="grid grid-cols-4 gap-6 mb-8">
                <div class="bg-white rounded-lg shadow-md p-6">
                    <p class="text-sm text-gray-500 mb-2">์ด ์‹œํ—˜ ์ˆ˜</p>
                    <p class="text-3xl font-bold">{{ $stats['total_exams'] }}</p>
                </div>
                <div class="bg-white rounded-lg shadow-md p-6">
                    <p class="text-sm text-gray-500 mb-2">์ด ์‘์‹œ ํšŸ์ˆ˜</p>
                    <p class="text-3xl font-bold">{{ $stats['total_attempts'] }}</p>
                </div>
                <div class="bg-white rounded-lg shadow-md p-6">
                    <p class="text-sm text-gray-500 mb-2">ํ‰๊ท  ํ•ฉ๊ฒฉ๋ฅ </p>
                    <p class="text-3xl font-bold text-green-600">{{ round($stats['pass_rate'], 1) }}%</p>
                </div>
                <div class="bg-white rounded-lg shadow-md p-6">
                    <p class="text-sm text-gray-500 mb-2">ํ‰๊ท  ์ ์ˆ˜</p>
                    <p class="text-3xl font-bold text-blue-600">{{ round($stats['avg_score'], 1) }}์ </p>
                </div>
            </div>

            <div class="grid grid-cols-2 gap-6">
                <!-- ์‹œํ—˜๋ณ„ ํ†ต๊ณ„ -->
                <div class="bg-white rounded-lg shadow-md p-6">
                    <h2 class="text-xl font-bold mb-4">์‹œํ—˜๋ณ„ ํ†ต๊ณ„</h2>
                    <canvas id="examChart"></canvas>
                </div>

                <!-- ์ตœ๊ทผ ์‘์‹œ ๊ธฐ๋ก -->
                <div class="bg-white rounded-lg shadow-md p-6">
                    <h2 class="text-xl font-bold mb-4">์ตœ๊ทผ ์‘์‹œ ๊ธฐ๋ก</h2>
                    <div class="space-y-2">
                        @foreach($recentAttempts as $attempt)
                            <div class="flex justify-between items-center py-2 border-b">
                                <div>
                                    <p class="font-bold text-sm">{{ $attempt->user->name }}</p>
                                    <p class="text-xs text-gray-500">{{ $attempt->exam->title }}</p>
                                </div>
                                <div class="text-right">
                                    <p class="font-bold">{{ $attempt->score }}์ </p>
                                    @if($attempt->passed)
                                        <span class="text-xs bg-green-100 text-green-800 px-2 py-1 rounded">ํ•ฉ๊ฒฉ</span>
                                    @else
                                        <span class="text-xs bg-red-100 text-red-800 px-2 py-1 rounded">๋ถˆํ•ฉ๊ฒฉ</span>
                                    @endif
                                </div>
                            </div>
                        @endforeach
                    </div>
                </div>
            </div>
        </div>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <script>
        const ctx = document.getElementById('examChart').getContext('2d');
        new Chart(ctx, {
            type: 'bar',
            data: {
                labels: @json($examStats->pluck('title')),
                datasets: [{
                    label: 'ํ‰๊ท  ์ ์ˆ˜',
                    data: @json($examStats->pluck('avg_score')),
                    backgroundColor: 'rgba(59, 130, 246, 0.5)',
                    borderColor: 'rgb(59, 130, 246)',
                    borderWidth: 1
                }]
            },
            options: {
                responsive: true,
                scales: {
                    y: {
                        beginAtZero: true,
                        max: 100
                    }
                }
            }
        });
    </script>
</x-app-layout>
```

4. **๋„ค๋น„๊ฒŒ์ด์…˜์— ๊ด€๋ฆฌ์ž ๋ฉ”๋‰ด ์ถ”๊ฐ€** (`resources/views/layouts/navigation.blade.php`):
```blade
@if(auth()->user()->role === 'admin')
    <x-nav-link :href="route('admin.dashboard')" :active="request()->routeIs('admin.dashboard')">
        ๊ด€๋ฆฌ์ž ๋Œ€์‹œ๋ณด๋“œ
    </x-nav-link>
    <x-nav-link :href="route('admin.exams.index')" :active="request()->routeIs('admin.exams.*')">
        ์‹œํ—˜ ๊ด€๋ฆฌ
    </x-nav-link>
@endif
```

5. **๋ถ€์ •ํ–‰์œ„ ๋ฐฉ์ง€ ๊ธฐ๋Šฅ ์ถ”๊ฐ€** (์‹œํ—˜ ์‘์‹œ ํŽ˜์ด์ง€์— ํƒญ ์ „ํ™˜ ๊ฐ์ง€):

`resources/views/exams/take.blade.php`์˜ Alpine.js ๋ฐ์ดํ„ฐ์— ์ถ”๊ฐ€:
```javascript
warnings: 0,

init() {
    // ๊ธฐ์กด init ์ฝ”๋“œ...

    // ํƒญ ์ „ํ™˜ ๊ฐ์ง€
    document.addEventListener('visibilitychange', () => {
        if (document.hidden) {
            this.warnings++;
            if (this.warnings >= 3) {
                alert('๋ถ€์ •ํ–‰์œ„๊ฐ€ ์˜์‹ฌ๋˜์–ด ์‹œํ—˜์ด ์ž๋™ ์ œ์ถœ๋ฉ๋‹ˆ๋‹ค.');
                document.querySelector('form').submit();
            } else {
                alert(`๊ฒฝ๊ณ : ํƒญ ์ „ํ™˜์ด ๊ฐ์ง€๋˜์—ˆ์Šต๋‹ˆ๋‹ค. (${this.warnings}/3)`);
            }
        }
    });
}
```

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ๊ด€๋ฆฌ์ž ๋Œ€์‹œ๋ณด๋“œ ๊ตฌํ˜„ ์™„๋ฃŒ. ์ด ์‹œํ—˜ ์ˆ˜, ์‘์‹œ ํšŸ์ˆ˜, ํ•ฉ๊ฒฉ๋ฅ , ํ‰๊ท  ์ ์ˆ˜ ํ†ต๊ณ„ ํ‘œ์‹œ. Chart.js๋กœ ์‹œํ—˜๋ณ„ ํ‰๊ท  ์ ์ˆ˜ ์ฐจํŠธ ์‹œ๊ฐํ™”. ์ตœ๊ทผ ์‘์‹œ ๊ธฐ๋ก ๋ชฉ๋ก ํ‘œ์‹œ. ๋„ค๋น„๊ฒŒ์ด์…˜์— ๊ด€๋ฆฌ์ž ๋ฉ”๋‰ด ์ถ”๊ฐ€. ์‹œํ—˜ ์‘์‹œ ์ค‘ ํƒญ ์ „ํ™˜ ๊ฐ์ง€ ๋ฐ ๊ฒฝ๊ณ  ๊ธฐ๋Šฅ ๊ตฌํ˜„.

10
Step 10
์ตœ์ข… ๋งˆ๋ฌด๋ฆฌ ๋ฐ UI/UX ๊ฐœ์„ :

1. **๋‹คํฌ๋ชจ๋“œ ํ† ๊ธ€ ์ถ”๊ฐ€** (`resources/views/layouts/app.blade.php`):

`<head>` ํƒœ๊ทธ ์•ˆ์— ๋‹คํฌ๋ชจ๋“œ ์Šคํฌ๋ฆฝํŠธ ์ถ”๊ฐ€:
```html
<script>
    if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
        document.documentElement.classList.add('dark')
    } else {
        document.documentElement.classList.remove('dark')
    }
</script>
```

๋„ค๋น„๊ฒŒ์ด์…˜์— ๋‹คํฌ๋ชจ๋“œ ํ† ๊ธ€ ๋ฒ„ํŠผ ์ถ”๊ฐ€:
```blade
<button @click="toggleDarkMode()" class="p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800">
    <span x-show="!darkMode">๐ŸŒ™</span>
    <span x-show="darkMode">โ˜€๏ธ</span>
</button>

<script>
    function toggleDarkMode() {
        if (document.documentElement.classList.contains('dark')) {
            document.documentElement.classList.remove('dark');
            localStorage.theme = 'light';
        } else {
            document.documentElement.classList.add('dark');
            localStorage.theme = 'dark';
        }
    }
</script>
```

2. **Tailwind ๋‹คํฌ๋ชจ๋“œ ์„ค์ •** (`tailwind.config.js`):
```javascript
module.exports = {
    darkMode: 'class',
    // ... ๊ธฐ์กด ์„ค์ •
}
```

3. **๋ฐ˜์‘ํ˜• ๊ฐœ์„  ๋ฐ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ถ”๊ฐ€**:

์‹œํ—˜ ์นด๋“œ ํ˜ธ๋ฒ„ ํšจ๊ณผ ๊ฐ•ํ™”:
```blade
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 hover:shadow-xl hover:scale-105 transition-all duration-300">
```

ํƒ€์ด๋จธ ์• ๋‹ˆ๋ฉ”์ด์…˜:
```blade
<p class="text-2xl font-bold transition-colors duration-300" 
   :class="timeLeft < 300 ? 'text-red-600 animate-pulse' : 'text-green-600'" 
   x-text="formatTime(timeLeft)">
</p>
```

4. **์—๋Ÿฌ ํ•ธ๋“ค๋ง ๊ฐœ์„ **:

`app/Exceptions/Handler.php`์—์„œ 403, 404 ์—๋Ÿฌ ์ปค์Šคํ…€ ํŽ˜์ด์ง€ ์ถ”๊ฐ€:
```php
public function render($request, Throwable $exception)
{
    if ($exception instanceof \Symfony\Component\HttpKernel\Exception\NotFoundHttpException) {
        return response()->view('errors.404', [], 404);
    }

    if ($exception instanceof \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException) {
        return response()->view('errors.403', [], 403);
    }

    return parent::render($request, $exception);
}
```

5. **README.md ์ž‘์„ฑ**:
```markdown
# ๐ŸŽ“ ์˜จ๋ผ์ธ ์ž๊ฒฉ์ฆ ์‹œํ—˜ ํ”Œ๋žซํผ

Laravel 13.x + Blade ํ…œํ”Œ๋ฆฟ์œผ๋กœ ๊ตฌํ˜„ํ•œ ์‹ค์‹œ๊ฐ„ ํƒ€์ด๋จธ ๊ธฐ๋ฐ˜ ์˜จ๋ผ์ธ ์‹œํ—˜ ์‘์‹œ ์‹œ์Šคํ…œ์ž…๋‹ˆ๋‹ค.

## ์ฃผ์š” ๊ธฐ๋Šฅ
- โœ… ์‹ค์‹œ๊ฐ„ ์นด์šดํŠธ๋‹ค์šด ํƒ€์ด๋จธ (์‹œ๊ฐ„ ์ดˆ๊ณผ ์‹œ ์ž๋™ ์ œ์ถœ)
- โœ… ๊ฐ๊ด€์‹/์ฃผ๊ด€์‹ ๋ฌธ์ œ ์ง€์›
- โœ… ์ž๋™ ์ฑ„์  ์‹œ์Šคํ…œ
- โœ… ํ•ฉ๊ฒฉ/๋ถˆํ•ฉ๊ฒฉ ํŒ์ • ๋ฐ ๊ฒฐ๊ณผ ๋ฆฌํฌํŠธ
- โœ… ๊ด€๋ฆฌ์ž ์‹œํ—˜/๋ฌธ์ œ ๊ด€๋ฆฌ CRUD
- โœ… ์‘์‹œ ๊ธฐ๋ก ๋ฐ ํ†ต๊ณ„ ๋Œ€์‹œ๋ณด๋“œ
- โœ… ๋ถ€์ •ํ–‰์œ„ ๋ฐฉ์ง€ (ํƒญ ์ „ํ™˜ ๊ฐ์ง€)
- โœ… ๋‹คํฌ๋ชจ๋“œ ์ง€์›

## ์„ค์น˜ ๋ฐฉ๋ฒ•

1. ์˜์กด์„ฑ ์„ค์น˜:
```bash
composer install
npm install
```

2. ํ™˜๊ฒฝ ์„ค์ •:
```bash
cp .env.example .env
php artisan key:generate
```

3. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋ฐ ์‹œ๋”:
```bash
touch database/database.sqlite
php artisan migrate --seed
```

4. ๊ฐœ๋ฐœ ์„œ๋ฒ„ ์‹คํ–‰:
```bash
php artisan serve
npm run dev
```

## ๊ธฐ๋ณธ ๊ณ„์ •
- **๊ด€๋ฆฌ์ž**: admin@example.com / password
- **์‘์‹œ์ž**: examinee@example.com / password

## ๊ธฐ์ˆ  ์Šคํƒ
- Laravel 13.x
- Blade ํ…œํ”Œ๋ฆฟ
- Tailwind CSS 4.x
- Alpine.js
- Chart.js
- SQLite

## ๋ผ์ด์„ ์Šค
MIT License
```

6. **์ตœ์ข… ํ…Œ์ŠคํŠธ**:
```bash
# ์บ์‹œ ํด๋ฆฌ์–ด
php artisan config:clear
php artisan cache:clear
php artisan view:clear

# ํ”„๋กœ๋•์…˜ ๋นŒ๋“œ
npm run build

# ๋ชจ๋“  ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ:
# 1. ํšŒ์›๊ฐ€์ž…/๋กœ๊ทธ์ธ
# 2. ์‹œํ—˜ ๋ชฉ๋ก ์กฐํšŒ ๋ฐ ํ•„ํ„ฐ๋ง
# 3. ์‹œํ—˜ ์‘์‹œ (ํƒ€์ด๋จธ ํ™•์ธ)
# 4. ๋‹ต์•ˆ ์ž๋™ ์ €์žฅ ํ™•์ธ
# 5. ์‹œํ—˜ ์ œ์ถœ ๋ฐ ๊ฒฐ๊ณผ ํ™•์ธ
# 6. ๊ด€๋ฆฌ์ž ๋กœ๊ทธ์ธ ํ›„ ์‹œํ—˜/๋ฌธ์ œ CRUD
# 7. ํ†ต๊ณ„ ๋Œ€์‹œ๋ณด๋“œ ํ™•์ธ
# 8. ๋‹คํฌ๋ชจ๋“œ ํ† ๊ธ€
```

๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ์ตœ์ข… ๋งˆ๋ฌด๋ฆฌ ์™„๋ฃŒ. ๋‹คํฌ๋ชจ๋“œ ํ† ๊ธ€ ๊ธฐ๋Šฅ ์ถ”๊ฐ€. ๋ฐ˜์‘ํ˜• ๋””์ž์ธ ๋ฐ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๊ฐ•ํ™”. ์—๋Ÿฌ ํŽ˜์ด์ง€ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•. README.md ์ž‘์„ฑ์œผ๋กœ ํ”„๋กœ์ ํŠธ ๋ฌธ์„œํ™”. ๋ชจ๋“  ๊ธฐ๋Šฅ์ด ์ •์ƒ ์ž‘๋™ํ•˜๋Š” ์™„์„ฑ๋œ ์˜จ๋ผ์ธ ์ž๊ฒฉ์ฆ ์‹œํ—˜ ํ”Œ๋žซํผ ๊ตฌํ˜„ ์™„๋ฃŒ.

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

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

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

๋Œ“๊ธ€ (0)

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