๐ ์จ๋ผ์ธ ๊ฐ์ ํ๋ซํผ - LMS ํ์คํ ๊ตฌํ with ๋์์ ์คํธ๋ฆฌ๋ฐ
online-course-lms-platform
๋ฌธ์ ์ค๋ช
๐ ํ๋ก์ ํธ ๊ฐ์
์ค๋ฌด ์์ค์ ์จ๋ผ์ธ ๊ฐ์ ํ๋ซํผ(LMS)์ Laravel 13.x + Blade๋ก ๊ตฌ์ถํฉ๋๋ค. ๊ฐ์ฌ๋ ๊ฐ์๋ฅผ ๋ฑ๋กํ๊ณ ์๊ฐ์์ ์ฝ์ค๋ฅผ ๊ตฌ๋งคํ์ฌ ํ์ตํ ์ ์์ต๋๋ค. ๋์์ ์คํธ๋ฆฌ๋ฐ, ์ง๋์จ ์ถ์ , ์๋ฃ์ฆ ๋ฐ๊ธ, ๊ฒฐ์ ์ฐ๋ ๋ฑ ์ค์ ์๋น์ค์ ํ์ํ ํต์ฌ ๊ธฐ๋ฅ์ ๋ชจ๋ ํฌํจํฉ๋๋ค.
โจ ํ์ ๊ธฐ๋ฅ
๐จโ๐ซ ๊ฐ์ฌ ๊ธฐ๋ฅ
- ๊ฐ์ ์ฝ์ค ์์ฑ/์์ /์ญ์ (์ ๋ชฉ, ์ค๋ช , ๊ฐ๊ฒฉ, ์ธ๋ค์ผ)
- ์ฑํฐ๋ณ ๋ ์จ ๊ตฌ์ฑ (๋๋๊ทธ์ค๋๋กญ์ผ๋ก ์์ ๋ณ๊ฒฝ)
- ๋์์ ์ ๋ก๋ ๋ฐ ์คํธ๋ฆฌ๋ฐ (Laravel Media Library + HLS)
- ํ์ต ์๋ฃ ์ฒจ๋ถ (PDF, ZIP ๋ฑ)
- ์๊ฐ์ ์ง๋์จ ๋ฐ ํต๊ณ ๋์๋ณด๋
- ์์ต ๋ฆฌํฌํธ (์๋ณ ๋งค์ถ, ํ๋ถ ๋ด์ญ)
๐ ์๊ฐ์ ๊ธฐ๋ฅ
- ๊ฐ์ ๊ฒ์ ๋ฐ ์นดํ ๊ณ ๋ฆฌ๋ณ ํ์
- ๊ฐ์ ์์ธ ํ์ด์ง (์ปค๋ฆฌํ๋ผ ๋ฏธ๋ฆฌ๋ณด๊ธฐ, ์ํ ์์)
- ์ฅ๋ฐ๊ตฌ๋ ๋ฐ ์์๋ฆฌ์คํธ
- ๊ฒฐ์ ์ฐ๋ (Stripe/Toss Payments)
- ํ์ต ํ์ด์ง (๋์์ ํ๋ ์ด์ด, ๋ฐฐ์ ์กฐ์ , ๋ถ๋งํฌ)
- ์ง๋์จ ์๋ ์ถ์ (์์ฒญ ์๊ฐ ๊ธฐ๋ฐ)
- ์๋ฃ์ฆ ์๋ ๋ฐ๊ธ (100% ์๊ฐ ์ PDF ์์ฑ)
- Q&A ๊ฒ์ํ (๊ฐ์๋ณ ์ง๋ฌธ/๋ต๋ณ)
๐ ์ธ์ฆ ๋ฐ ๊ถํ
- ํ์๊ฐ์ /๋ก๊ทธ์ธ (Laravel Fortify)
- ์ญํ ๊ธฐ๋ฐ ์ ๊ทผ ์ ์ด (๊ฐ์ฌ/์๊ฐ์/๊ด๋ฆฌ์)
- ์ด๋ฉ์ผ ์ธ์ฆ ๋ฐ ๋น๋ฐ๋ฒํธ ์ฌ์ค์
- ์์ ๋ก๊ทธ์ธ (Google, GitHub)
๐ณ ๊ฒฐ์ ๋ฐ ํ๋ถ
- Stripe ๊ฒฐ์ ์ฐ๋ (์นด๋, ๊ฐํธ๊ฒฐ์ )
- ์ฃผ๋ฌธ ๋ด์ญ ๋ฐ ์์์ฆ ๋ฐ๊ธ
- ํ๋ถ ์ ์ฑ (7์ผ ์ด๋ด ์ ์ก ํ๋ถ)
- ์ฟ ํฐ ๋ฐ ํ ์ธ ์ฝ๋ ์์คํ
๐ ๊ณ ๊ธ ๊ธฐ๋ฅ
- Redis ์บ์ฑ (๊ฐ์ ๋ชฉ๋ก, ์ธ๊ธฐ ์ฝ์ค)
- Laravel Queue๋ก ๋์์ ์ธ์ฝ๋ฉ ๋ฐฑ๊ทธ๋ผ์ด๋ ์ฒ๋ฆฌ
- ์๋ฆผ ์์คํ (์ ๋ ์จ ์ ๋ก๋, ์ง๋ฌธ ๋ต๋ณ ์๋ฆผ)
- ๊ฒ์ ์์ง ์ต์ ํ (Laravel Scout + Meilisearch)
- ๋ค๊ตญ์ด ์ง์ (ํ๊ตญ์ด/์์ด)
๐ ๋ณด๋์ค ๊ธฐ๋ฅ
- ๊ฐ์ ๋ฆฌ๋ทฐ ๋ฐ ๋ณ์ ์์คํ
- ์๊ฐ์ ํ์ต ํต๊ณ (์ผ์ผ ํ์ต ์๊ฐ, ์๊ฐ๋ฅ )
- ๊ฐ์ฌ ์ธ์ฆ ๋ฑ์ง (๋ฒ ์คํธ ๊ฐ์ฌ)
- ๋ผ์ด๋ธ Q&A ์ธ์ ์์ฝ (Laravel Reverb)
- AI ๊ธฐ๋ฐ ๊ฐ์ ์ถ์ฒ (OpenAI API)
- ์๊ฐ์ ๋ ธํธ ์์ฑ (ํ์์คํฌํ๋ณ ๋ฉ๋ชจ)
- ๋ชจ๋ฐ์ผ ๋ฐ์ํ ๋์์ธ (Tailwind CSS)
๐ฏ ํ์ต ๋ชฉํ
- Laravel ๋๊ท๋ชจ ํ๋ก์ ํธ ์ํคํ ์ฒ ์ค๊ณ
- ํ์ผ ์ ๋ก๋ ๋ฐ ์คํธ๋ฆฌ๋ฐ ์ฒ๋ฆฌ
- ๊ฒฐ์ ์ฐ๋ ๋ฐ ๋ณด์ ์ฒ๋ฆฌ
- ํ/์ก์ ํ์ฉํ ๋น๋๊ธฐ ์์
- ์บ์ฑ ์ ๋ต ๋ฐ ์ฑ๋ฅ ์ต์ ํ
- ๋ณต์กํ DB ๊ด๊ณ ์ค๊ณ (๋ค๋๋ค, ํด๋ฆฌ๋ชจํฝ)
- ํ ์คํธ ์ฃผ๋ ๊ฐ๋ฐ (PHPUnit)
์ ์ฝ ์กฐ๊ฑด
- โข Laravel 13.x ์ต์ ๋ฒ์ ์ฌ์ฉ
- โข Blade ํ ํ๋ฆฟ ์์ง ์ฌ์ฉ (React/Vue ๊ธ์ง)
- โข Tailwind CSS๋ก UI ๊ตฌ์ฑ
- โข SQLite (๊ฐ๋ฐ) / PostgreSQL (ํ๋ก๋์ ) ์ฌ์ฉ
- โข Laravel Fortify๋ก ์ธ์ฆ ๊ตฌํ
- โข Stripe API ์ฐ๋ (ํ ์คํธ ๋ชจ๋)
- โข Laravel Media Library๋ก ํ์ผ ๊ด๋ฆฌ
- โข Redis ์บ์ฑ ํ์
- โข Laravel Queue๋ก ๋ฐฑ๊ทธ๋ผ์ด๋ ์์ ์ฒ๋ฆฌ
- โข PHPUnit ํ ์คํธ ์ปค๋ฒ๋ฆฌ์ง 70% ์ด์
- โข ๋ฐ์ํ ๋์์ธ (๋ชจ๋ฐ์ผ ์ฐ์ )
- โข ์ ๊ทผ์ฑ ์ค์ (ARIA ๋ ์ด๋ธ)
- โข ๋ณด์: CSRF ํ ํฐ, XSS ๋ฐฉ์ง, SQL Injection ๋ฐฉ์ง
ํ๋กฌํํธ ๋ ์ํผ
์๋ ํ๋กฌํํธ๋ฅผ ์์๋๋ก AI์๊ฒ ๋ณด๋ด๋ฉด ๋ฉ๋๋ค. ๋ณต์ฌ ๋ฒํผ์ ๋๋ฌ ๋ฐ๋ก ์ฌ์ฉํ์ธ์.
mkdir online-course-lms-platform && cd online-course-lms-platform ์ด ํด๋์์ Laravel 13.x ๊ธฐ๋ฐ ์จ๋ผ์ธ ๊ฐ์ ํ๋ซํผ ํ๋ก์ ํธ๋ฅผ ์์ํฉ๋๋ค. ## ์๊ตฌ์ฌํญ 1. Laravel 13.x ์ต์ ๋ฒ์ ์ผ๋ก ํ๋ก์ ํธ ์์ฑ 2. ๊ธฐ๋ณธ ์ค์ ํ์ผ ๊ตฌ์ฑ: - .env ํ์ผ์ SQLite ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค์ - APP_NAME์ 'CourseHub'๋ก ๋ณ๊ฒฝ - Redis ์บ์ ๋๋ผ์ด๋ฒ ์ค์ - Queue ๋๋ผ์ด๋ฒ๋ฅผ 'database'๋ก ์ค์ 3. ํ์ ํจํค์ง ์ค์น: - laravel/fortify (์ธ์ฆ) - spatie/laravel-medialibrary (ํ์ผ ๊ด๋ฆฌ) - spatie/laravel-permission (๊ถํ ๊ด๋ฆฌ) - stripe/stripe-php (๊ฒฐ์ ) - barryvdh/laravel-dompdf (PDF ์์ฑ) 4. Tailwind CSS ์ค์ (Laravel Mix ๋๋ Vite) 5. ๊ธฐ๋ณธ ๋๋ ํ ๋ฆฌ ๊ตฌ์กฐ ์์ฑ: - app/Models (User, Course, Lesson, Enrollment, Payment ๋ฑ) - app/Http/Controllers (Auth, Course, Lesson, Payment ๋ฑ) - resources/views/layouts (app.blade.php, guest.blade.php) - resources/views/components (Blade ์ปดํฌ๋ํธ) 6. Git ์ด๊ธฐํ ๋ฐ .gitignore ์ค์ ํ๋ก์ ํธ ์์ฑ ํ README.md์ ์ค์น ๊ฐ์ด๋๋ฅผ ์์ฑํ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: Laravel 13.x ํ๋ก์ ํธ๊ฐ ์์ฑ๋๊ณ , ํ์ ํจํค์ง๊ฐ ์ค์น๋ฉ๋๋ค. Tailwind CSS๊ฐ ์ค์ ๋๊ณ , ๊ธฐ๋ณธ ๋๋ ํ ๋ฆฌ ๊ตฌ์กฐ๊ฐ ์ค๋น๋ฉ๋๋ค. .env ํ์ผ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ฐ ์บ์ ์ค์ ์ด ์๋ฃ๋ฉ๋๋ค.
๋ฐ์ดํฐ๋ฒ ์ด์ค ์คํค๋ง๋ฅผ ์ค๊ณํ๊ณ ๋ง์ด๊ทธ๋ ์ด์ ํ์ผ์ ์์ฑํ์ธ์. ## ํ์ํ ํ ์ด๋ธ ๋ฐ ๊ด๊ณ ### users ํ ์ด๋ธ - id, name, email, password, email_verified_at - role (enum: 'student', 'instructor', 'admin') - avatar (nullable) - bio (text, nullable) - timestamps ### courses ํ ์ด๋ธ - id, instructor_id (foreign key to users) - title, slug (unique) - description (text) - price (decimal) - thumbnail (nullable) - category (enum: 'programming', 'design', 'business', 'marketing', 'etc') - level (enum: 'beginner', 'intermediate', 'advanced') - is_published (boolean, default false) - timestamps, soft deletes ### lessons ํ ์ด๋ธ - id, course_id (foreign key) - chapter_id (nullable, foreign key to chapters) - title, slug - description (text, nullable) - video_path (nullable) - duration (integer, ์ด ๋จ์) - order (integer) - is_free (boolean, ์ํ ์์ ์ฌ๋ถ) - timestamps ### chapters ํ ์ด๋ธ - id, course_id (foreign key) - title - order (integer) - timestamps ### enrollments ํ ์ด๋ธ (์๊ฐ ๋ฑ๋ก) - id, user_id, course_id - enrolled_at (timestamp) - completed_at (nullable timestamp) - progress (decimal, 0-100) - unique constraint on (user_id, course_id) - timestamps ### lesson_progress ํ ์ด๋ธ - id, enrollment_id, lesson_id - watched_duration (integer, ์ด ๋จ์) - is_completed (boolean) - last_watched_at (timestamp) - timestamps ### payments ํ ์ด๋ธ - id, user_id, course_id - stripe_payment_id (unique) - amount (decimal) - currency (default 'krw') - status (enum: 'pending', 'completed', 'refunded') - refunded_at (nullable timestamp) - timestamps ### reviews ํ ์ด๋ธ - id, user_id, course_id - rating (integer, 1-5) - comment (text, nullable) - timestamps ### qna_posts ํ ์ด๋ธ - id, user_id, course_id, lesson_id (nullable) - title, content (text) - is_answered (boolean, default false) - timestamps ### qna_answers ํ ์ด๋ธ - id, post_id (foreign key), user_id - content (text) - is_instructor_answer (boolean) - timestamps ## ์ถ๊ฐ ์๊ตฌ์ฌํญ 1. ์ธ๋ ํค ์ ์ฝ ์กฐ๊ฑด ์ค์ (onDelete cascade/restrict) 2. ์ธ๋ฑ์ค ์ถ๊ฐ (slug, instructor_id, course_id ๋ฑ) 3. Eloquent ๋ชจ๋ธ ์์ฑ ๋ฐ ๊ด๊ณ ์ ์: - User hasMany Courses (as instructor) - User belongsToMany Courses through Enrollments - Course hasMany Lessons, Reviews, Enrollments - Course belongsTo User (instructor) - Lesson belongsTo Course, Chapter - Enrollment belongsTo User, Course - Enrollment hasMany LessonProgress 4. ๋ชจ๋ธ์ fillable/guarded ์์ฑ ์ ์ 5. ๋ชจ๋ธ์ casts ์ ์ (JSON, datetime ๋ฑ) php artisan migrate ์คํ ์ ๋ง์ด๊ทธ๋ ์ด์ ํ์ผ์ ๊ฒํ ํ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: ๋ชจ๋ ํ ์ด๋ธ ๋ง์ด๊ทธ๋ ์ด์ ํ์ผ์ด ์์ฑ๋๊ณ , Eloquent ๋ชจ๋ธ์ด ๊ด๊ณ์ ํจ๊ป ์ ์๋ฉ๋๋ค. ๋ฐ์ดํฐ๋ฒ ์ด์ค ์คํค๋ง๊ฐ ๋ ผ๋ฆฌ์ ์ผ๋ก ์ค๊ณ๋์ด ๊ฐ์ ํ๋ซํผ์ ํต์ฌ ๊ธฐ๋ฅ์ ์ง์ํฉ๋๋ค.
์ธ์ฆ ์์คํ ๊ณผ ์ญํ ๊ธฐ๋ฐ ์ ๊ทผ ์ ์ด(RBAC)๋ฅผ ๊ตฌํํ์ธ์. ## ์๊ตฌ์ฌํญ ### 1. Laravel Fortify ์ค์ - Fortify ์ค์น ๋ฐ ์ค์ - ํ์๊ฐ์ , ๋ก๊ทธ์ธ, ๋ก๊ทธ์์ ๋ผ์ฐํธ ํ์ฑํ - ์ด๋ฉ์ผ ์ธ์ฆ ํ์ฑํ - ๋น๋ฐ๋ฒํธ ์ฌ์ค์ ํ์ฑํ - 2๋จ๊ณ ์ธ์ฆ (์ ํ ์ฌํญ) ### 2. Spatie Permission ์ค์ - Role ์์ฑ: 'student', 'instructor', 'admin' - Permission ์ ์: - 'create-course', 'edit-course', 'delete-course' - 'view-analytics', 'manage-users' - 'refund-payment' - Seeder ์์ฑ ๋ฐ ๊ธฐ๋ณธ ์ญํ /๊ถํ ํ ๋น ### 3. ์ปค์คํ ๋ฏธ๋ค์จ์ด - EnsureUserIsInstructor ๋ฏธ๋ค์จ์ด ์์ฑ - EnsureUserIsStudent ๋ฏธ๋ค์จ์ด ์์ฑ - ๋ผ์ฐํธ์ ๋ฏธ๋ค์จ์ด ์ ์ฉ ### 4. ์ธ์ฆ ๋ทฐ (Blade) - resources/views/auth/login.blade.php (Tailwind CSS) - resources/views/auth/register.blade.php - ํ์๊ฐ์ ์ ์ญํ ์ ํ (๊ฐ์ฌ/์๊ฐ์) - resources/views/auth/forgot-password.blade.php - resources/views/auth/verify-email.blade.php ### 5. ๋ ์ด์์ ๋ฐ ๋ค๋น๊ฒ์ด์ - resources/views/layouts/app.blade.php - ๋ก๊ทธ์ธ/๋ก๊ทธ์์ ์ํ์ ๋ฐ๋ฅธ ๋ค๋น๊ฒ์ด์ ๋ฐ - ์ญํ ์ ๋ฐ๋ผ ๋ค๋ฅธ ๋ฉ๋ด ํ์ (๊ฐ์ฌ๋ '๋ด ๊ฐ์', ์๊ฐ์์ '๋ด ํ์ต') - resources/views/layouts/guest.blade.php (๋น๋ก๊ทธ์ธ ์ฌ์ฉ์์ฉ) ### 6. UserSeeder ์์ฑ - ํ ์คํธ์ฉ ๊ด๋ฆฌ์ ๊ณ์ (admin@example.com) - ํ ์คํธ์ฉ ๊ฐ์ฌ ๊ณ์ (instructor@example.com) - ํ ์คํธ์ฉ ์๊ฐ์ ๊ณ์ (student@example.com) ### 7. ๋ผ์ฐํธ ์ ์ - routes/web.php์ ์ธ์ฆ ๊ด๋ จ ๋ผ์ฐํธ ๊ทธ๋ฃน ์์ฑ - ๋ฏธ๋ค์จ์ด๋ก ๋ณดํธ๋ ๋ผ์ฐํธ ์ค์ ## ๋์์ธ ์๊ตฌ์ฌํญ - Tailwind CSS๋ก ๊น๋ํ ํผ ๋์์ธ - ๋ฐ์ํ ๋ ์ด์์ - ์ ํจ์ฑ ๊ฒ์ฌ ์๋ฌ ๋ฉ์์ง ํ์ - ์ฑ๊ณต/์คํจ ํ๋์ ๋ฉ์์ง (session flash) Seeder ์คํ ํ ํ ์คํธ ๊ณ์ ์ผ๋ก ๋ก๊ทธ์ธ ๊ฐ๋ฅํ์ง ํ์ธํ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: Laravel Fortify ๊ธฐ๋ฐ ์ธ์ฆ ์์คํ ์ด ๊ตฌ์ถ๋๊ณ , Spatie Permission์ผ๋ก ์ญํ ๊ธฐ๋ฐ ์ ๊ทผ ์ ์ด๊ฐ ์ค์ ๋ฉ๋๋ค. ๋ก๊ทธ์ธ/ํ์๊ฐ์ ํ์ด์ง๊ฐ Tailwind CSS๋ก ๋์์ธ๋๊ณ , ์ญํ ์ ๋ฐ๋ผ ๋ค๋ฅธ ๋ค๋น๊ฒ์ด์ ์ด ํ์๋ฉ๋๋ค.
๊ฐ์ฌ์ฉ ๊ฐ์ ๊ด๋ฆฌ CRUD ๊ธฐ๋ฅ์ ๊ตฌํํ์ธ์.
## ์๊ตฌ์ฌํญ
### 1. CourseController ์์ฑ
- InstructorCourseController (๊ฐ์ฌ ์ ์ฉ)
- ๋ฉ์๋: index, create, store, edit, update, destroy
- Policy ์์ฑ: CoursePolicy (๊ฐ์ฌ๋ง ์์ ์ ๊ฐ์ ์์ ๊ฐ๋ฅ)
### 2. ๊ฐ์ ๋ชฉ๋ก ํ์ด์ง
- resources/views/instructor/courses/index.blade.php
- ๊ฐ์ฌ๊ฐ ์์ฑํ ๋ชจ๋ ๊ฐ์ ํ์ (ํ
์ด๋ธ ๋๋ ์นด๋ ํ์)
- ๊ฐ ๊ฐ์๋ณ ์ ๋ณด:
- ์ธ๋ค์ผ, ์ ๋ชฉ, ์นดํ
๊ณ ๋ฆฌ, ๊ฐ๊ฒฉ
- ์๊ฐ์ ์, ์ด ๋งค์ถ
- ์ํ (๊ณต๊ฐ/๋น๊ณต๊ฐ)
- ์์ /์ญ์ ๋ฒํผ
- '์ ๊ฐ์ ๋ง๋ค๊ธฐ' ๋ฒํผ
- ํ์ด์ง๋ค์ด์
(10๊ฐ์ฉ)
### 3. ๊ฐ์ ์์ฑ ํ์ด์ง
- resources/views/instructor/courses/create.blade.php
- ํผ ํ๋:
- ์ ๋ชฉ (required)
- ์ค๋ช
(Markdown ์๋ํฐ ๋๋ Textarea)
- ์นดํ
๊ณ ๋ฆฌ ์ ํ (๋๋กญ๋ค์ด)
- ๋์ด๋ ์ ํ (๋ผ๋์ค ๋ฒํผ)
- ๊ฐ๊ฒฉ (์ซ์ ์
๋ ฅ, KRW)
- ์ธ๋ค์ผ ์
๋ก๋ (์ด๋ฏธ์ง ๋ฏธ๋ฆฌ๋ณด๊ธฐ)
- ์ ํจ์ฑ ๊ฒ์ฌ: FormRequest ํด๋์ค ์์ฑ
- ์ธ๋ค์ผ ์
๋ก๋: Spatie Media Library ์ฌ์ฉ
### 4. ๊ฐ์ ์์ ํ์ด์ง
- resources/views/instructor/courses/edit.blade.php
- ๊ธฐ์กด ๋ฐ์ดํฐ ๋ก๋ํ์ฌ ํผ์ ํ์
- ์ธ๋ค์ผ ๋ณ๊ฒฝ ๊ธฐ๋ฅ (๊ธฐ์กด ์ด๋ฏธ์ง ์ ์ง ๋๋ ์ ์ด๋ฏธ์ง ์
๋ก๋)
- Slug ์๋ ์์ฑ (์ ๋ชฉ ๊ธฐ๋ฐ, ์ค๋ณต ๋ฐฉ์ง)
### 5. ๊ฐ์ ์ญ์
- Soft Delete ์ฌ์ฉ (๊ฐ์ ๋ณต๊ตฌ ๊ฐ๋ฅ)
- ์ญ์ ์ ํ์ธ ๋ชจ๋ฌ (Alpine.js ๋๋ Blade ์ปดํฌ๋ํธ)
- ์๊ฐ์์ด ์๋ ๊ฐ์๋ ์ญ์ ๋ถ๊ฐ (์ ์ฑ
๊ฒ์ฆ)
### 6. ๋ผ์ฐํธ ์ ์
- routes/web.php์ instructor ๊ทธ๋ฃน ์ถ๊ฐ
- Route::middleware(['auth', 'instructor'])->group()
- ๋ฆฌ์์ค ๋ผ์ฐํธ: Route::resource('courses', InstructorCourseController::class)
### 7. Blade ์ปดํฌ๋ํธ
- <x-course-card> ์ปดํฌ๋ํธ (๊ฐ์ ์นด๋ UI)
- <x-form-input> ์ปดํฌ๋ํธ (์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ์
๋ ฅ ํ๋)
- <x-alert> ์ปดํฌ๋ํธ (์ฑ๊ณต/์๋ฌ ๋ฉ์์ง)
## ๋์์ธ ์๊ตฌ์ฌํญ
- Tailwind CSS๋ก ๋ชจ๋ํ ๋์๋ณด๋ ๋์์ธ
- ๋ฐ์ํ ๋ ์ด์์ (๋ชจ๋ฐ์ผ์์๋ ์ฌ์ฉ ๊ฐ๋ฅ)
- ์ธ๋ค์ผ ์
๋ก๋ ์ ๋๋๊ทธ์ค๋๋กญ ์ง์
- ๋ก๋ฉ ์ํ ํ์ (ํผ ์ ์ถ ์)
CRUD ์์ฑ ํ ๊ฐ์ฌ ๊ณ์ ์ผ๋ก ๋ก๊ทธ์ธํ์ฌ ๊ฐ์ ์์ฑ/์์ /์ญ์ ๋ฅผ ํ
์คํธํ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: ๊ฐ์ฌ๊ฐ ๊ฐ์๋ฅผ ์์ฑ, ์์ , ์ญ์ ํ ์ ์๋ CRUD ์์คํ ์ด ์์ฑ๋ฉ๋๋ค. Spatie Media Library๋ก ์ธ๋ค์ผ์ ๊ด๋ฆฌํ๊ณ , Policy๋ก ๊ถํ์ ์ ์ดํฉ๋๋ค. Tailwind CSS๋ก ๊น๋ํ ๋์๋ณด๋๊ฐ ๊ตฌํ๋ฉ๋๋ค.
์ฑํฐ ๋ฐ ๋ ์จ ๊ด๋ฆฌ ๊ธฐ๋ฅ์ ๊ตฌํํ์ธ์ (๋๋๊ทธ์ค๋๋กญ ์์ ๋ณ๊ฒฝ ํฌํจ).
## ์๊ตฌ์ฌํญ
### 1. ChapterController ๋ฐ LessonController ์์ฑ
- InstructorChapterController
- InstructorLessonController
- ์ค์ฒฉ ๋ผ์ฐํธ ์ฌ์ฉ: courses/{course}/chapters, courses/{course}/lessons
### 2. ์ฑํฐ ๊ด๋ฆฌ
- ์ฑํฐ CRUD (์์ฑ, ์์ , ์ญ์ )
- ์ฑํฐ ์์ ๋ณ๊ฒฝ (๋๋๊ทธ์ค๋๋กญ)
- ์ฑํฐ๋ณ ๋ ์จ ๊ทธ๋ฃนํ
### 3. ๋ ์จ ๊ด๋ฆฌ ํ์ด์ง
- resources/views/instructor/courses/lessons/index.blade.php
- ์ฑํฐ๋ณ๋ก ๋ ์จ ๋ชฉ๋ก ํ์ (์์ฝ๋์ธ ๋๋ ํญ)
- ๋๋๊ทธ์ค๋๋กญ์ผ๋ก ๋ ์จ ์์ ๋ณ๊ฒฝ (SortableJS ๋๋ Alpine.js)
- ๊ฐ ๋ ์จ ์ ๋ณด:
- ์ ๋ชฉ, ์๊ฐ, ๋ฌด๋ฃ ์ฌ๋ถ
- ์์ /์ญ์ ๋ฒํผ
### 4. ๋ ์จ ์์ฑ/์์ ํ์ด์ง
- resources/views/instructor/courses/lessons/create.blade.php
- resources/views/instructor/courses/lessons/edit.blade.php
- ํผ ํ๋:
- ์ ๋ชฉ (required)
- ์ค๋ช
(optional)
- ์ฑํฐ ์ ํ (๋๋กญ๋ค์ด, ์์ผ๋ฉด ๋ฏธ์ง์ )
- ๋์์ ์
๋ก๋ (MP4, ์ต๋ 500MB)
- ๋ฌด๋ฃ ๊ณต๊ฐ ์ฌ๋ถ (์ฒดํฌ๋ฐ์ค)
- ํ์ต ์๋ฃ ์ฒจ๋ถ (PDF, ZIP ๋ฑ)
### 5. ๋์์ ์
๋ก๋ ๋ฐ ์ฒ๋ฆฌ
- Spatie Media Library๋ก ๋์์ ์ ์ฅ
- Laravel Queue๋ก ๋ฐฑ๊ทธ๋ผ์ด๋์์ ๋์์ ์ธ์ฝ๋ฉ
- Job ํด๋์ค: ProcessVideoUpload
- FFmpeg ์ฌ์ฉํ์ฌ HLS ํฌ๋งท์ผ๋ก ๋ณํ (์ ํ ์ฌํญ)
- ์ธ๋ค์ผ ์๋ ์์ฑ
- ์์ ๊ธธ์ด(duration) ์๋ ์ถ์ถ
- ์
๋ก๋ ์งํ๋ฅ ํ์ (Livewire ๋๋ JavaScript fetch)
### 6. ๋๋๊ทธ์ค๋๋กญ ์์ ๋ณ๊ฒฝ
- SortableJS ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ
- AJAX ์์ฒญ์ผ๋ก ์์ ์
๋ฐ์ดํธ
- POST /courses/{course}/lessons/reorder
- JSON payload: [{ id: 1, order: 1 }, { id: 2, order: 2 }]
- Optimistic UI ์
๋ฐ์ดํธ (์ฆ์ ๋ฐ์)
### 7. ํ์ต ์๋ฃ ๊ด๋ฆฌ
- ๋ ์จ๋ณ ํ์ผ ์ฒจ๋ถ (PDF, ZIP, ์ด๋ฏธ์ง ๋ฑ)
- ํ์ผ ๋ค์ด๋ก๋ ๋ผ์ฐํธ (์ธ์ฆ ํ์)
- ํ์ผ ํฌ๊ธฐ ์ ํ (10MB)
### 8. ๋ผ์ฐํธ ์ ์
- Route::resource('courses.chapters', InstructorChapterController::class)
- Route::resource('courses.lessons', InstructorLessonController::class)
- Route::post('courses/{course}/lessons/reorder', [InstructorLessonController::class, 'reorder'])
## ๋์์ธ ์๊ตฌ์ฌํญ
- ๋๋๊ทธ์ค๋๋กญ ์๊ฐ์ ํผ๋๋ฐฑ (๊ทธ๋ฆผ์, ์ปค์ ๋ณ๊ฒฝ)
- ๋์์ ์
๋ก๋ ์ค ํ๋ก๊ทธ๋ ์ค ๋ฐ
- ๋ฐ์ํ ๋ ์ด์์
๋์์ ์
๋ก๋ ํ Queue Worker๋ฅผ ์คํํ์ฌ ์ธ์ฝ๋ฉ์ด ์ ์ ์๋ํ๋์ง ํ์ธํ์ธ์ (php artisan queue:work).
๊ธฐ๋ ๊ฒฐ๊ณผ: ์ฑํฐ์ ๋ ์จ์ ์์ฑ, ์์ , ์ญ์ ํ ์ ์์ผ๋ฉฐ, ๋๋๊ทธ์ค๋๋กญ์ผ๋ก ์์๋ฅผ ๋ณ๊ฒฝํ ์ ์์ต๋๋ค. ๋์์ ์ ๋ก๋ ์ Laravel Queue๋ก ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์ธ์ฝ๋ฉ์ด ์ฒ๋ฆฌ๋ฉ๋๋ค. SortableJS๋ก ์ง๊ด์ ์ธ UI๊ฐ ๊ตฌํ๋ฉ๋๋ค.
์๊ฐ์์ฉ ๊ฐ์ ํ์ ๋ฐ ์์ธ ํ์ด์ง๋ฅผ ๊ตฌํํ์ธ์.
## ์๊ตฌ์ฌํญ
### 1. CourseController (์๊ฐ์์ฉ)
- PublicCourseController ์์ฑ
- ๋ฉ์๋: index, show
### 2. ๊ฐ์ ๋ชฉ๋ก ํ์ด์ง
- resources/views/courses/index.blade.php
- ํํฐ๋ง ๊ธฐ๋ฅ:
- ์นดํ
๊ณ ๋ฆฌ๋ณ ํํฐ (์ฌ์ด๋๋ฐ ๋๋ ํญ)
- ๋์ด๋๋ณ ํํฐ (์ฒดํฌ๋ฐ์ค)
- ๊ฐ๊ฒฉ ๋ฒ์ ํํฐ (์ฌ๋ผ์ด๋)
- ๊ฒ์ ๊ธฐ๋ฅ (์ ๋ชฉ, ์ค๋ช
)
- ์ ๋ ฌ ์ต์
:
- ์ต์ ์, ์ธ๊ธฐ์ (์๊ฐ์ ์), ํ์ ๋์ ์, ๊ฐ๊ฒฉ ๋ฎ์ ์
- ๊ฐ์ ์นด๋ ํ์:
- ์ธ๋ค์ผ, ์ ๋ชฉ, ๊ฐ์ฌ๋ช
- ๊ฐ๊ฒฉ, ํ์ (๋ณ ์์ด์ฝ)
- ์๊ฐ์ ์, ๋ ์จ ์
- ํ์ด์ง๋ค์ด์
(12๊ฐ์ฉ)
- ๋ฌดํ ์คํฌ๋กค ์ต์
(์ ํ ์ฌํญ)
### 3. ๊ฐ์ ์์ธ ํ์ด์ง
- resources/views/courses/show.blade.php
- ์๋จ ์น์
:
- ์ธ๋ค์ผ (๋๋ ์ํ ์์ ํ๋ ์ด์ด)
- ์ ๋ชฉ, ๊ฐ์ฌ ์ ๋ณด (์๋ฐํ, ์ด๋ฆ, ์๊ฐ)
- ๊ฐ๊ฒฉ, ํ์ , ์๊ฐ์ ์
- '์ฅ๋ฐ๊ตฌ๋ ๋ด๊ธฐ', '์์๋ฆฌ์คํธ ์ถ๊ฐ', '๋ฐ๋ก ๊ตฌ๋งค' ๋ฒํผ
- ํญ ๋ฉ๋ด:
- ๊ฐ์ ์๊ฐ (description)
- ์ปค๋ฆฌํ๋ผ (์ฑํฐ ๋ฐ ๋ ์จ ๋ชฉ๋ก, ๋ฌด๋ฃ ๋ ์จ ํ์)
- ๋ฆฌ๋ทฐ (๋ณ์ ๋ถํฌ ์ฐจํธ, ์ต์ ๋ฆฌ๋ทฐ 5๊ฐ)
- ๊ฐ์ฌ ์ ๋ณด
- Q&A (์ง๋ฌธ ๋ชฉ๋ก)
- ์ปค๋ฆฌํ๋ผ ์น์
:
- ์์ฝ๋์ธ์ผ๋ก ์ฑํฐ ํผ์น๊ธฐ/์ ๊ธฐ
- ๊ฐ ๋ ์จ: ์ ๋ชฉ, ์๊ฐ, ๋ฌด๋ฃ ์ฌ๋ถ
- ๋ฌด๋ฃ ๋ ์จ์ '๋ฏธ๋ฆฌ๋ณด๊ธฐ' ๋ฒํผ (๋ชจ๋ฌ๋ก ์์ ์ฌ์)
- ์ํ ์์ ๋ชจ๋ฌ:
- Video.js ๋๋ Plyr.js ํ๋ ์ด์ด
- ์ ์ฒดํ๋ฉด ์ง์
### 4. ๊ฒ์ ๊ธฐ๋ฅ
- Laravel Scout + Meilisearch ์ค์
- Course ๋ชจ๋ธ์ Searchable trait ์ถ๊ฐ
- ๊ฒ์ ๊ฒฐ๊ณผ ํ์ด์ง (resources/views/courses/search.blade.php)
- ์๋์์ฑ (Alpine.js ๋๋ JavaScript fetch)
### 5. ์์๋ฆฌ์คํธ ๊ธฐ๋ฅ
- wishlist ํ
์ด๋ธ ์์ฑ (user_id, course_id, unique constraint)
- WishlistController (add, remove)
- ์์๋ฆฌ์คํธ ํ์ด์ง (resources/views/wishlist/index.blade.php)
- ํํธ ์์ด์ฝ ํ ๊ธ (AJAX)
### 6. ๋ผ์ฐํธ ์ ์
- Route::get('/courses', [PublicCourseController::class, 'index'])->name('courses.index')
- Route::get('/courses/{course:slug}', [PublicCourseController::class, 'show'])->name('courses.show')
- Route::post('/wishlist/{course}', [WishlistController::class, 'add'])->middleware('auth')
- Route::delete('/wishlist/{course}', [WishlistController::class, 'remove'])->middleware('auth')
## ๋์์ธ ์๊ตฌ์ฌํญ
- Tailwind CSS๋ก ์ด์ปค๋จธ์ค ์คํ์ผ ๋์์ธ
- ๋ฐ์ํ ๋ ์ด์์ (๋ชจ๋ฐ์ผ์์ ํญ์ ๋๋กญ๋ค์ด์ผ๋ก)
- ์ด๋ฏธ์ง Lazy Loading
- ์ค์ผ๋ ํค ๋ก๋ฉ (๋ฐ์ดํฐ ๋ก๋ ์ )
Meilisearch ์ค์ ๋ฐ ์ธ๋ฑ์ฑ ํ ๊ฒ์ ๊ธฐ๋ฅ์ ํ
์คํธํ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: ์๊ฐ์์ด ๊ฐ์๋ฅผ ํ์ํ๊ณ , ํํฐ๋ง/๊ฒ์ํ๊ณ , ์์ธ ์ ๋ณด๋ฅผ ํ์ธํ ์ ์๋ ํ์ด์ง๊ฐ ์์ฑ๋ฉ๋๋ค. Laravel Scout๋ก ๋น ๋ฅธ ๊ฒ์์ด ๊ฐ๋ฅํ๊ณ , ์์๋ฆฌ์คํธ ๊ธฐ๋ฅ์ด ์ถ๊ฐ๋ฉ๋๋ค. Tailwind CSS๋ก ๊น๋ํ ์ด์ปค๋จธ์ค UI๊ฐ ๊ตฌํ๋ฉ๋๋ค.
Stripe ๊ฒฐ์ ์ฐ๋ ๋ฐ ์ฅ๋ฐ๊ตฌ๋ ๊ธฐ๋ฅ์ ๊ตฌํํ์ธ์.
## ์๊ตฌ์ฌํญ
### 1. Stripe ์ค์
- Stripe PHP SDK ์ค์น (composer require stripe/stripe-php)
- .env์ Stripe API ํค ์ถ๊ฐ (ํ
์คํธ ๋ชจ๋)
- STRIPE_PUBLIC_KEY
- STRIPE_SECRET_KEY
- config/services.php์ Stripe ์ค์ ์ถ๊ฐ
### 2. ์ฅ๋ฐ๊ตฌ๋ ๊ธฐ๋ฅ
- cart ํ
์ด๋ธ ์์ฑ (user_id, course_id, added_at)
- CartController (index, add, remove, clear)
- resources/views/cart/index.blade.php
- ์ฅ๋ฐ๊ตฌ๋์ ๋ด๊ธด ๊ฐ์ ๋ชฉ๋ก
- ๊ฐ ๊ฐ์: ์ธ๋ค์ผ, ์ ๋ชฉ, ๊ฐ๊ฒฉ, ์ญ์ ๋ฒํผ
- ์ด ๊ธ์ก ํ์
- '๊ฒฐ์ ํ๊ธฐ' ๋ฒํผ
### 3. ๊ฒฐ์ ํ์ด์ง
- resources/views/checkout/index.blade.php
- Stripe Checkout Session ์์ฑ
- PaymentController::createCheckoutSession()
- ์ฅ๋ฐ๊ตฌ๋ ์์ดํ
์ Stripe Line Items๋ก ๋ณํ
- ์ฑ๊ณต/์คํจ ๋ฆฌ๋ค์ด๋ ํธ URL ์ค์
- Stripe Elements ํตํฉ (์นด๋ ์
๋ ฅ ํผ)
- ๊ฒฐ์ ๋ฒํผ ํด๋ฆญ ์ Stripe Checkout ํ์ด์ง๋ก ๋ฆฌ๋ค์ด๋ ํธ
### 4. ๊ฒฐ์ ์๋ฃ ์ฒ๋ฆฌ
- Stripe Webhook ์ค์
- Route::post('/webhook/stripe', [WebhookController::class, 'handleStripe'])
- Webhook ์๋ช
๊ฒ์ฆ
- checkout.session.completed ์ด๋ฒคํธ ์ฒ๋ฆฌ:
- Payment ๋ ์ฝ๋ ์์ฑ (status: 'completed')
- Enrollment ๋ ์ฝ๋ ์์ฑ (์๊ฐ ๋ฑ๋ก)
- ์ฅ๋ฐ๊ตฌ๋ ๋น์ฐ๊ธฐ
- ์ด๋ฉ์ผ ์๋ฆผ ๋ฐ์ก (๊ตฌ๋งค ํ์ธ ๋ฉ์ผ)
### 5. ๊ฒฐ์ ์ฑ๊ณต/์คํจ ํ์ด์ง
- resources/views/checkout/success.blade.php
- ๊ตฌ๋งค ์๋ฃ ๋ฉ์์ง
- ๊ตฌ๋งคํ ๊ฐ์ ๋ชฉ๋ก
- '๋ด ํ์ต ํ์ด์ง๋ก ์ด๋' ๋ฒํผ
- resources/views/checkout/cancel.blade.php
- ๊ฒฐ์ ์ทจ์ ๋ฉ์์ง
- '์ฅ๋ฐ๊ตฌ๋๋ก ๋์๊ฐ๊ธฐ' ๋ฒํผ
### 6. ํ๋ถ ๊ธฐ๋ฅ
- PaymentController::refund()
- 7์ผ ์ด๋ด ์ ์ก ํ๋ถ ์ ์ฑ
๊ฒ์ฆ
- Stripe Refund API ํธ์ถ
- Payment ์ํ ์
๋ฐ์ดํธ (status: 'refunded')
- Enrollment ์ญ์ (ํ์ต ์ง๋์จ 10% ์ดํ์ผ ๊ฒฝ์ฐ๋ง)
- ํ๋ถ ์๋ฆผ ์ด๋ฉ์ผ ๋ฐ์ก
### 7. ์ฃผ๋ฌธ ๋ด์ญ ํ์ด์ง
- resources/views/orders/index.blade.php
- ์ฌ์ฉ์์ ๋ชจ๋ ์ฃผ๋ฌธ ๋ด์ญ ํ์
- ๊ฐ ์ฃผ๋ฌธ: ๊ฐ์๋ช
, ๊ฒฐ์ ์ผ, ๊ธ์ก, ์ํ
- ์์์ฆ ๋ค์ด๋ก๋ ๋ฒํผ (PDF)
- ํ๋ถ ์์ฒญ ๋ฒํผ (์กฐ๊ฑด ์ถฉ์กฑ ์)
### 8. ์ฟ ํฐ ์์คํ
- coupons ํ
์ด๋ธ ์์ฑ (code, discount_type, discount_value, expires_at)
- ์ฅ๋ฐ๊ตฌ๋ ํ์ด์ง์ ์ฟ ํฐ ์
๋ ฅ ํ๋
- ์ฟ ํฐ ๊ฒ์ฆ ๋ฐ ํ ์ธ ์ ์ฉ
- Stripe Checkout์ ์ฟ ํฐ ์ ๋ณด ์ ๋ฌ
## ๋ณด์ ์๊ตฌ์ฌํญ
- CSRF ํ ํฐ ๊ฒ์ฆ
- Webhook ์๋ช
๊ฒ์ฆ (Stripe::constructEvent)
- ์ค๋ณต ๊ฒฐ์ ๋ฐฉ์ง (idempotency key)
- ๊ฒฐ์ ๊ธ์ก ์๋ฒ ๊ฒ์ฆ (ํด๋ผ์ด์ธํธ ์กฐ์ ๋ฐฉ์ง)
## ๋์์ธ ์๊ตฌ์ฌํญ
- Tailwind CSS๋ก ์ฒดํฌ์์ ํ์ด์ง ๋์์ธ
- ๊ฒฐ์ ์งํ ๋จ๊ณ ํ์ (ํ๋ก๊ทธ๋ ์ค ๋ฐ)
- ๋ก๋ฉ ์ํ ํ์ (๊ฒฐ์ ์ฒ๋ฆฌ ์ค)
Stripe CLI๋ก Webhook ํ
์คํธ ํ ์ค์ ๊ฒฐ์ ํ๋ก์ฐ๋ฅผ ํ์ธํ์ธ์ (stripe listen --forward-to localhost:8000/webhook/stripe).
๊ธฐ๋ ๊ฒฐ๊ณผ: Stripe ๊ฒฐ์ ๊ฐ ์์ ํ ํตํฉ๋๊ณ , ์ฅ๋ฐ๊ตฌ๋, ์ฒดํฌ์์, ๊ฒฐ์ ์๋ฃ, ํ๋ถ ๊ธฐ๋ฅ์ด ๋ชจ๋ ๊ตฌํ๋ฉ๋๋ค. Webhook์ผ๋ก ์์ ํ๊ฒ ๊ฒฐ์ ์ด๋ฒคํธ๋ฅผ ์ฒ๋ฆฌํ๊ณ , ์ฟ ํฐ ์์คํ ๋ ์ถ๊ฐ๋ฉ๋๋ค. ๋ณด์ ๊ฒ์ฆ์ด ์ฒ ์ ํ ์ด๋ฃจ์ด์ง๋๋ค.
์๊ฐ์์ฉ ํ์ต ํ์ด์ง์ ์ง๋์จ ์ถ์ ์์คํ
์ ๊ตฌํํ์ธ์.
## ์๊ตฌ์ฌํญ
### 1. ํ์ต ํ์ด์ง
- resources/views/learn/show.blade.php
- ๋ ์ด์์:
- ์ผ์ชฝ: ๋์์ ํ๋ ์ด์ด (Video.js ๋๋ Plyr.js)
- ์ค๋ฅธ์ชฝ ์ฌ์ด๋๋ฐ: ์ปค๋ฆฌํ๋ผ ๋ชฉ๋ก (์ฑํฐ ๋ฐ ๋ ์จ)
- ํ๋จ: ๋ ์จ ์ค๋ช
, ํ์ต ์๋ฃ ๋ค์ด๋ก๋ ๋งํฌ
- ํ๋ ์ด์ด ๊ธฐ๋ฅ:
- ์ฌ์/์ผ์์ ์ง, ๋ณผ๋ฅจ ์กฐ์
- ๋ฐฐ์ ์กฐ์ (0.5x, 1x, 1.25x, 1.5x, 2x)
- ์ ์ฒดํ๋ฉด ๋ชจ๋
- ํค๋ณด๋ ๋จ์ถํค (์คํ์ด์ค๋ฐ: ์ฌ์/์ผ์์ ์ง, ํ์ดํ: ์๋ค ์ด๋)
- PIP (Picture-in-Picture) ๋ชจ๋
- ์ปค๋ฆฌํ๋ผ ์ฌ์ด๋๋ฐ:
- ์๋ฃํ ๋ ์จ ์ฒดํฌ ํ์
- ํ์ฌ ์ฌ์ ์ค์ธ ๋ ์จ ๊ฐ์กฐ
- ๋ค์ ๋ ์จ ์๋ ์ฌ์ ์ต์
(ํ ๊ธ)
### 2. ์ง๋์จ ์ถ์
- LessonProgressController ์์ฑ
- ์์ฒญ ์๊ฐ ๊ธฐ๋ก:
- JavaScript๋ก 10์ด๋ง๋ค ํ์ฌ ์ฌ์ ์๊ฐ์ ์๋ฒ์ ์ ์ก
- POST /lessons/{lesson}/progress
- JSON payload: { watched_duration: 120 }
- ๋ ์จ ์๋ฃ ์กฐ๊ฑด:
- ์์ฒญ ์๊ฐ์ด ์ ์ฒด ๊ธธ์ด์ 90% ์ด์์ผ ๋ ์๋ ์๋ฃ
- is_completed ํ๋๊ทธ ์
๋ฐ์ดํธ
- ๊ฐ์ ์ ์ฒด ์ง๋์จ ๊ณ์ฐ:
- (์๋ฃํ ๋ ์จ ์ / ์ ์ฒด ๋ ์จ ์) * 100
- Enrollment ๋ชจ๋ธ์ progress ํ๋ ์
๋ฐ์ดํธ
### 3. ๋ถ๋งํฌ ๊ธฐ๋ฅ
- lesson_bookmarks ํ
์ด๋ธ ์์ฑ (user_id, lesson_id, timestamp, note)
- ํ๋ ์ด์ด์์ '๋ถ๋งํฌ ์ถ๊ฐ' ๋ฒํผ
- ํ์ฌ ์ฌ์ ์๊ฐ๊ณผ ๋ฉ๋ชจ ์ ์ฅ
- ๋ถ๋งํฌ ํด๋ฆญ ์ ํด๋น ์๊ฐ์ผ๋ก ์ด๋
### 4. ๋
ธํธ ์์ฑ ๊ธฐ๋ฅ
- lesson_notes ํ
์ด๋ธ ์์ฑ (user_id, lesson_id, timestamp, content)
- ํ์ต ํ์ด์ง ํ๋จ์ ๋
ธํธ ์๋ํฐ (Markdown ์ง์)
- ํ์์คํฌํ๋ณ ๋
ธํธ ๋ชฉ๋ก ํ์
- ๋
ธํธ ํด๋ฆญ ์ ํด๋น ์๊ฐ์ผ๋ก ์ด๋
### 5. ๋ด ํ์ต ๋์๋ณด๋
- resources/views/dashboard/learning.blade.php
- ์๊ฐ ์ค์ธ ๊ฐ์ ๋ชฉ๋ก:
- ๊ฐ ๊ฐ์: ์ธ๋ค์ผ, ์ ๋ชฉ, ์ง๋์จ (ํ๋ก๊ทธ๋ ์ค ๋ฐ)
- '์ด์ด์ ํ์ตํ๊ธฐ' ๋ฒํผ (๋ง์ง๋ง ์์ฒญ ๋ ์จ์ผ๋ก ์ด๋)
- ํ์ต ํต๊ณ:
- ์ด ํ์ต ์๊ฐ (์ฃผ๋ณ, ์๋ณ ์ฐจํธ)
- ์๊ฐํ ๊ฐ์ ์
- ์งํ ์ค์ธ ๊ฐ์ ์
- ์ต๊ทผ ์์ฒญํ ๋ ์จ (5๊ฐ)
### 6. ์๋ฃ์ฆ ๋ฐ๊ธ
- CertificateController::generate()
- ์ง๋์จ 100% ๋ฌ์ฑ ์ ์๋ ๋ฐ๊ธ
- Laravel DomPDF๋ก PDF ์์ฑ
- ํ
ํ๋ฆฟ: resources/views/certificates/template.blade.php
- ๋ด์ฉ: ์๊ฐ์ ์ด๋ฆ, ๊ฐ์๋ช
, ์๊ฐ์ผ, ๊ฐ์ฌ ์๋ช
- ์๋ฃ์ฆ ๋ค์ด๋ก๋ ๋ฒํผ (ํ์ต ๋์๋ณด๋)
### 7. ๋ผ์ฐํธ ์ ์
- Route::get('/learn/{enrollment}', [LearnController::class, 'show'])->middleware('auth')
- Route::post('/lessons/{lesson}/progress', [LessonProgressController::class, 'update'])
- Route::post('/lessons/{lesson}/bookmarks', [BookmarkController::class, 'store'])
- Route::get('/certificates/{enrollment}', [CertificateController::class, 'download'])
## ๋์์ธ ์๊ตฌ์ฌํญ
- ํ๋ ์ด์ด ์ค์ฌ์ ๋ชฐ์
ํ ๋ ์ด์์
- ๋คํฌ ๋ชจ๋ ์ง์ (ํ ๊ธ ๋ฒํผ)
- ๋ฐ์ํ (๋ชจ๋ฐ์ผ์์๋ ์ฌ์ด๋๋ฐ๊ฐ ํ๋จ์ผ๋ก ์ด๋)
๋์์ ์ฌ์ ์ค ์ง๋์จ์ด ์ ์์ ์ผ๋ก ์
๋ฐ์ดํธ๋๋์ง ํ์ธํ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: ์๊ฐ์์ด ๋์์์ ์์ฒญํ๊ณ , ์ง๋์จ์ด ์๋์ผ๋ก ์ถ์ ๋๋ฉฐ, ๋ถ๋งํฌ์ ๋ ธํธ๋ฅผ ์์ฑํ ์ ์๋ ํ์ต ํ์ด์ง๊ฐ ์์ฑ๋ฉ๋๋ค. 100% ์๊ฐ ์ ์๋ฃ์ฆ์ด ์๋ ๋ฐ๊ธ๋ฉ๋๋ค. Video.js๋ก ํ๋ถํ ํ๋ ์ด์ด ๊ธฐ๋ฅ์ด ์ ๊ณต๋ฉ๋๋ค.
๋ฆฌ๋ทฐ ์์คํ
๊ณผ Q&A ๊ฒ์ํ์ ๊ตฌํํ์ธ์.
## ์๊ตฌ์ฌํญ
### 1. ๋ฆฌ๋ทฐ ์์คํ
- ReviewController (store, update, destroy)
- ๋ฆฌ๋ทฐ ์์ฑ ์กฐ๊ฑด:
- ๊ฐ์๋ฅผ ๊ตฌ๋งคํ ์๊ฐ์๋ง ๊ฐ๋ฅ
- ์ง๋์จ 10% ์ด์์ผ ๋๋ง ๊ฐ๋ฅ
- 1์ธ๋น 1๊ฐ์ ๋ฆฌ๋ทฐ๋ง ์์ฑ ๊ฐ๋ฅ (์์ ์ ๊ฐ๋ฅ)
- ๋ฆฌ๋ทฐ ํผ (๊ฐ์ ์์ธ ํ์ด์ง):
- ๋ณ์ (1-5, ๋ณ ์์ด์ฝ ํด๋ฆญ)
- ์ฝ๋ฉํธ (Textarea, ์ต์ 20์)
- '๋ฆฌ๋ทฐ ์์ฑ' ๋ฒํผ
- ๋ฆฌ๋ทฐ ๋ชฉ๋ก (๊ฐ์ ์์ธ ํ์ด์ง ํญ):
- ์ต์ ์, ํ์ ๋์ ์, ํ์ ๋ฎ์ ์ ์ ๋ ฌ
- ๊ฐ ๋ฆฌ๋ทฐ: ์์ฑ์ ์๋ฐํ, ์ด๋ฆ, ๋ณ์ , ์์ฑ์ผ, ์ฝ๋ฉํธ
- ๋ณธ์ธ ๋ฆฌ๋ทฐ๋ ์์ /์ญ์ ๋ฒํผ ํ์
- ํ์ด์ง๋ค์ด์
(10๊ฐ์ฉ)
- ํ์ ํต๊ณ:
- ํ๊ท ํ์ (์์์ 1์๋ฆฌ)
- ๋ณ์ ๋ณ ๋ถํฌ (5์ : 50%, 4์ : 30%, ...)
- ์ด ๋ฆฌ๋ทฐ ์
### 2. Q&A ๊ฒ์ํ
- QnaController (index, create, store, show, update, destroy)
- QnaAnswerController (store, update, destroy)
- ์ง๋ฌธ ์์ฑ (๊ฐ์ ์์ธ ํ์ด์ง ๋๋ ํ์ต ํ์ด์ง):
- ์ ๋ชฉ, ๋ด์ฉ (Markdown ์ง์)
- ๋ ์จ ์ ํ (์ ํ ์ฌํญ)
- '์ง๋ฌธํ๊ธฐ' ๋ฒํผ
- ์ง๋ฌธ ๋ชฉ๋ก (๊ฐ์ ์์ธ ํ์ด์ง ํญ):
- resources/views/qna/index.blade.php
- ๊ฐ ์ง๋ฌธ: ์ ๋ชฉ, ์์ฑ์, ์์ฑ์ผ, ๋ต๋ณ ์, ํด๊ฒฐ ์ฌ๋ถ
- ๋ฏธํด๊ฒฐ ์ง๋ฌธ ์ฐ์ ํ์
- ๊ฒ์ ๊ธฐ๋ฅ (์ ๋ชฉ, ๋ด์ฉ)
- ํ์ด์ง๋ค์ด์
- ์ง๋ฌธ ์์ธ ํ์ด์ง:
- resources/views/qna/show.blade.php
- ์ง๋ฌธ ๋ด์ฉ (Markdown ๋ ๋๋ง)
- ๋ต๋ณ ๋ชฉ๋ก (์ต์ ์)
- ๋ต๋ณ ์์ฑ ํผ (Textarea)
- ๊ฐ์ฌ ๋ต๋ณ์ '๊ฐ์ฌ' ๋ฑ์ง ํ์
- ์ง๋ฌธ ์์ฑ์๋ ๋ต๋ณ์ '์ฑํ'ํ ์ ์์ (is_answered ์
๋ฐ์ดํธ)
- ๊ฐ์ฌ ์ ์ฉ ๊ธฐ๋ฅ:
- ๋ฏธ๋ต๋ณ ์ง๋ฌธ ์๋ฆผ (๋์๋ณด๋)
- ๋ต๋ณ ์์ฑ ์ ์ด๋ฉ์ผ ์๋ฆผ ๋ฐ์ก
### 3. ์๋ฆผ ์์คํ
- notifications ํ
์ด๋ธ ์์ฑ (Laravel ๊ธฐ๋ณธ ์๋ฆผ)
- ์๋ฆผ ํ์
:
- ์ ๋ฆฌ๋ทฐ ์์ฑ (๊ฐ์ฌ์๊ฒ)
- ์ ์ง๋ฌธ ์์ฑ (๊ฐ์ฌ์๊ฒ)
- ๋ต๋ณ ์์ฑ (์ง๋ฌธ ์์ฑ์์๊ฒ)
- ์ ๋ ์จ ์
๋ก๋ (์๊ฐ์์๊ฒ)
- ์๋ฆผ ํ์:
- ๋ค๋น๊ฒ์ด์
๋ฐ์ ์๋ฆผ ์์ด์ฝ (์ฝ์ง ์์ ์๋ฆผ ์ ํ์)
- ๋๋กญ๋ค์ด์ผ๋ก ์ต๊ทผ ์๋ฆผ 5๊ฐ ํ์
- '๋ชจ๋ ์ฝ์' ๋ฒํผ
- ์๋ฆผ ํ์ด์ง:
- resources/views/notifications/index.blade.php
- ๋ชจ๋ ์๋ฆผ ๋ชฉ๋ก (์ฝ์/์ฝ์ง ์์ ํํฐ)
### 4. ์ด๋ฉ์ผ ์๋ฆผ
- Laravel Mail ์ค์ (Mailtrap ๋๋ SMTP)
- Mailable ํด๋์ค ์์ฑ:
- NewReviewNotification (๊ฐ์ฌ์๊ฒ)
- NewQuestionNotification (๊ฐ์ฌ์๊ฒ)
- NewAnswerNotification (์ง๋ฌธ ์์ฑ์์๊ฒ)
- Queue๋ก ์ด๋ฉ์ผ ๋ฐ์ก (๋น๋๊ธฐ ์ฒ๋ฆฌ)
### 5. ๋ผ์ฐํธ ์ ์
- Route::resource('courses.reviews', ReviewController::class)->middleware('auth')
- Route::resource('courses.qna', QnaController::class)->middleware('auth')
- Route::post('qna/{qna}/answers', [QnaAnswerController::class, 'store'])
- Route::post('qna/{qna}/mark-answered', [QnaController::class, 'markAnswered'])
- Route::get('/notifications', [NotificationController::class, 'index'])
- Route::post('/notifications/mark-read', [NotificationController::class, 'markAllRead'])
## ๋์์ธ ์๊ตฌ์ฌํญ
- ๋ณ์ ์ ๋ณ ์์ด์ฝ์ผ๋ก ์๊ฐํ (ํด๋ฆญ ๊ฐ๋ฅ)
- Q&A๋ ํฌ๋ผ ์คํ์ผ ๋์์ธ
- ์๋ฆผ ๋๋กญ๋ค์ด์ ๋ถ๋๋ฌ์ด ์ ๋๋ฉ์ด์
- Markdown ๋ ๋๋ง (league/commonmark)
๋ฆฌ๋ทฐ ์์ฑ ํ ํ๊ท ํ์ ์ด ์๋ ์
๋ฐ์ดํธ๋๋์ง ํ์ธํ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: ๋ฆฌ๋ทฐ ์์คํ ๊ณผ Q&A ๊ฒ์ํ์ด ์์ฑ๋๊ณ , ์๋ฆผ ์์คํ ์ด ํตํฉ๋ฉ๋๋ค. ๊ฐ์ฌ์ ์๊ฐ์ ๊ฐ ์ํต์ด ์ํํ๊ฒ ์ด๋ฃจ์ด์ง๋ฉฐ, ์ด๋ฉ์ผ ์๋ฆผ์ด Queue๋ก ๋น๋๊ธฐ ๋ฐ์ก๋ฉ๋๋ค. Markdown์ผ๋ก ํ๋ถํ ์ฝํ ์ธ ์์ฑ์ด ๊ฐ๋ฅํฉ๋๋ค.
Redis ์บ์ฑ, ์ฑ๋ฅ ์ต์ ํ, ํ
์คํธ ์์ฑ์ ์๋ฃํ์ธ์.
## ์๊ตฌ์ฌํญ
### 1. Redis ์บ์ฑ
- Redis ์ค์น ๋ฐ Laravel ์ค์
- ์บ์ฑ ์ ๋ต:
- ๊ฐ์ ๋ชฉ๋ก (1์๊ฐ ์บ์ฑ)
- Cache::remember('courses.all', 3600, fn() => Course::with('instructor')->get())
- ์ธ๊ธฐ ๊ฐ์ (1์ผ ์บ์ฑ)
- Cache::remember('courses.popular', 86400, fn() => Course::orderBy('enrollments_count', 'desc')->take(10)->get())
- ๊ฐ์ ์์ธ ์ ๋ณด (30๋ถ ์บ์ฑ)
- Cache::remember("course.{$id}", 1800, fn() => Course::with('lessons', 'reviews')->find($id))
- ์บ์ ๋ฌดํจํ:
- ๊ฐ์ ์์ /์ญ์ ์ ์บ์ ์ญ์
- Cache::forget("course.{$id}")
- Cache::tags(['courses'])->flush()
### 2. ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ต์ ํ
- Eager Loading์ผ๋ก N+1 ์ฟผ๋ฆฌ ๋ฌธ์ ํด๊ฒฐ:
- Course::with('instructor', 'lessons', 'reviews')->get()
- ์ธ๋ฑ์ค ์ถ๊ฐ:
- courses ํ
์ด๋ธ: slug, instructor_id, category
- lessons ํ
์ด๋ธ: course_id, order
- enrollments ํ
์ด๋ธ: user_id, course_id
- ํ์ด์ง๋ค์ด์
์ต์ ํ (Cursor Pagination)
### 3. ์ด๋ฏธ์ง ์ต์ ํ
- Spatie Media Library๋ก ์ธ๋ค์ผ ์๋ ์์ฑ
- ์ฌ๋ฌ ํฌ๊ธฐ (small, medium, large)
- Lazy Loading ์ ์ฉ (loading="lazy")
- WebP ํฌ๋งท ๋ณํ (์ ํ ์ฌํญ)
### 4. ๋ณด์ ๊ฐํ
- CSRF ํ ํฐ ๊ฒ์ฆ (๋ชจ๋ POST ์์ฒญ)
- XSS ๋ฐฉ์ง ({{ }} Blade ์ด์ค์ผ์ดํ)
- SQL Injection ๋ฐฉ์ง (Eloquent ORM ์ฌ์ฉ)
- Rate Limiting:
- ๋ก๊ทธ์ธ ์๋: 5ํ/๋ถ
- ๋ฆฌ๋ทฐ ์์ฑ: 3ํ/์๊ฐ
- API ์์ฒญ: 60ํ/๋ถ
- HTTPS ๊ฐ์ (ํ๋ก๋์
)
### 5. ํ
์คํธ ์์ฑ (PHPUnit)
- Feature Tests:
- CourseTest (CRUD ํ
์คํธ)
- EnrollmentTest (์๊ฐ ๋ฑ๋ก ํ
์คํธ)
- PaymentTest (๊ฒฐ์ ํ๋ก์ฐ ํ
์คํธ)
- ReviewTest (๋ฆฌ๋ทฐ ์์ฑ ํ
์คํธ)
- Unit Tests:
- CoursePolicy (๊ถํ ํ
์คํธ)
- ProgressCalculation (์ง๋์จ ๊ณ์ฐ ํ
์คํธ)
- Database Factories ์์ฑ:
- UserFactory, CourseFactory, LessonFactory
- Seeder ์์ฑ:
- DatabaseSeeder (ํ
์คํธ ๋ฐ์ดํฐ ์์ฑ)
### 6. ์๋ฌ ํธ๋ค๋ง
- ์ปค์คํ
์๋ฌ ํ์ด์ง:
- resources/views/errors/404.blade.php
- resources/views/errors/500.blade.php
- ์์ธ ๋ก๊น
(Sentry ๋๋ Laravel Log)
- ์ฌ์ฉ์ ์นํ์ ์๋ฌ ๋ฉ์์ง
### 7. ํ๋ก๋์
์ค๋น
- .env.example ์
๋ฐ์ดํธ
- README.md ์์ฑ:
- ์ค์น ๊ฐ์ด๋
- ํ๊ฒฝ ๋ณ์ ์ค์
- ๋ง์ด๊ทธ๋ ์ด์
๋ฐ ์๋ฉ
- Queue Worker ์คํ ๋ฐฉ๋ฒ
- Composer ์ต์ ํ:
- composer install --optimize-autoloader --no-dev
- ์บ์ ์์ฑ:
- php artisan config:cache
- php artisan route:cache
- php artisan view:cache
### 8. ๋ฐฐํฌ ์ฒดํฌ๋ฆฌ์คํธ
- APP_ENV=production ์ค์
- APP_DEBUG=false ์ค์
- Stripe ํ๋ก๋์
ํค๋ก ๋ณ๊ฒฝ
- HTTPS ๊ฐ์ (AppServiceProvider)
- Queue Worker ๋ฐ๋ชฌ ์คํ (Supervisor)
- ์ค์ผ์ค๋ฌ ์ค์ (Cron)
- ๋ฐฑ์
์ค์ (Laravel Backup)
## ํ
์คํธ ์คํ
- php artisan test --coverage
- ์ปค๋ฒ๋ฆฌ์ง 70% ์ด์ ๋ชฉํ
๋ชจ๋ ํ
์คํธ๋ฅผ ํต๊ณผํ๊ณ , ์ฑ๋ฅ ์ต์ ํ๊ฐ ์๋ฃ๋๋ฉด ํ๋ก์ ํธ๊ฐ ์์ฑ๋ฉ๋๋ค!
๊ธฐ๋ ๊ฒฐ๊ณผ: Redis ์บ์ฑ์ผ๋ก ์ฑ๋ฅ์ด ์ต์ ํ๋๊ณ , ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฟผ๋ฆฌ๊ฐ ํจ์จ์ ์ผ๋ก ๊ฐ์ ๋ฉ๋๋ค. PHPUnit ํ ์คํธ๋ก ์ฝ๋ ํ์ง์ด ๊ฒ์ฆ๋๊ณ , ๋ณด์์ด ๊ฐํ๋ฉ๋๋ค. ํ๋ก๋์ ๋ฐฐํฌ ์ค๋น๊ฐ ์๋ฃ๋์ด ์ค์ ์๋น์ค๋ก ๋ฐ์นญํ ์ ์๋ ์์ค์ด ๋ฉ๋๋ค.
์ ์ถ๋ ์ํ (0)
๋ก๊ทธ์ธ ํ ์ ์ถ์์ง ์ ์ถ๋ ์ํ์ด ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ ๋์ ์๊ฐ ๋์ด๋ณด์ธ์!