๐ ์จ๋ผ์ธ ์ฝ์ค ์๊ฐ ํ๊ธฐ & ๊ฐ์ฌ ํ์ ํ๋ซํผ
course-review-rating-platform
๋ฌธ์ ์ค๋ช
๐ ํ๋ก์ ํธ ๊ฐ์
์จ๋ผ์ธ ๊ฐ์๋ฅผ ๋ค์ ์๊ฐ์๋ค์ด ์ฝ์ค๋ณ ํ๊ธฐ๋ฅผ ๋จ๊ธฐ๊ณ , ๊ฐ์ฌ์๊ฒ ํ์ ์ ๋งค๊ธฐ๋ ํ์คํ ํ๋ซํผ์ ๋๋ค. Laravel 13.x + Inertia.js + React 19 ๋ชจ๋ ธ๋ฆฌ์ค ์ํคํ ์ฒ๋ก ๊ตฌํํ๋ฉฐ, ์ค์ ์จ๋ผ์ธ ๊ต์ก ํ๋ซํผ์์ ์ฌ์ฉํ ์ ์๋ ์์ค์ ๋ฆฌ๋ทฐ ์์คํ ์ ๋ง๋ญ๋๋ค.
โจ ํต์ฌ ๊ธฐ๋ฅ
ํ์ ๊ธฐ๋ฅ:
- ๐ ์ฝ์ค ๋ชฉ๋ก ํ์ด์ง (์นด๋ ๊ทธ๋ฆฌ๋ ๋ ์ด์์, ํ๊ท ๋ณ์ ํ์)
- โญ ์ฝ์ค๋ณ ์์ธ ํ์ด์ง (๊ฐ์ฌ ์ ๋ณด, ์ปค๋ฆฌํ๋ผ, ์๊ฐ์ ๋ฆฌ๋ทฐ ๋ชฉ๋ก)
- โ๏ธ ๋ฆฌ๋ทฐ ์์ฑ ํผ (๋ณ์ 5์ ์ฒ๋, ์์ธ ํ๊ธฐ ํ ์คํธ, ์ถ์ฒ ์ฌ๋ถ ์ ํ)
- ๐ ๋ฆฌ๋ทฐ ํํฐ๋ง (๋ณ์ ๋ณ, ์ต์ ์/์ถ์ฒ์ ์ ๋ ฌ)
- ๐จโ๐ซ ๊ฐ์ฌ ํ๋กํ ํ์ด์ง (๋ด๋น ์ฝ์ค ๋ชฉ๋ก, ์ ์ฒด ํ๊ท ํ์ )
- ๐ ์ฝ์ค ํต๊ณ ๋์๋ณด๋ (์ด ๋ฆฌ๋ทฐ ์, ํ๊ท ๋ณ์ , ๋ณ์ ๋ถํฌ ์ฐจํธ)
๋ณด๋์ค ๊ธฐ๋ฅ:
- ๐ ๋ฆฌ๋ทฐ ๋์๋จ/๋์์๋จ ํฌํ ๊ธฐ๋ฅ
- ๐ ๋ฒ ์คํธ ๋ฆฌ๋ทฐ ๋ฐฐ์ง (์ข์์ 10๊ฐ ์ด์)
- ๐ ๊ฐ์ฌ ์๋ต ๊ธฐ๋ฅ (๋ฆฌ๋ทฐ์ ๋๊ธ ๋ฌ๊ธฐ)
- ๐ธ ๋ฆฌ๋ทฐ ์ด๋ฏธ์ง ์ฒจ๋ถ (์๊ฐ ์ธ์ฆ์ท)
- ๐ฏ ์๊ฐ ์๋ฃ ์ธ์ฆ ๋ฐฐ์ง (์ฝ์ค 100% ์๋ฃ ์์๋ง ๋ฆฌ๋ทฐ ์์ฑ ๊ฐ๋ฅ)
๐ฏ ํ์ต ๋ชฉํ
- Laravel Eloquent ๊ด๊ณํ ๋ชจ๋ธ ์ค๊ณ (Course โ Instructor โ Review)
- Inertia.js ํ์ด์ง ์ปดํฌ๋ํธ ๊ตฌ์กฐํ ๋ฐ ๋ฐ์ดํฐ ์ ๋ฌ
- React ์ํ ๊ด๋ฆฌ (๋ณ์ ์ ๋ ฅ, ํผ ๊ฒ์ฆ, ํํฐ ์ํ)
- ํ๊ท ๊ณ์ฐ ๋ฐ ์ง๊ณ ์ฟผ๋ฆฌ ์ต์ ํ
- ๋ฐ์ํ UI ๋์์ธ (Tailwind CSS ํ์ฉ)
- ํผ ๊ฒ์ฆ ๋ฐ ์๋ฌ ํธ๋ค๋ง (Laravel Validation + React ์ํ)
๐ ๏ธ ๊ธฐ์ ์คํ
- ๋ฐฑ์๋: Laravel 13.x (SQLite)
- ํ๋ก ํธ์๋: React 19 + Inertia.js
- ์คํ์ผ๋ง: Tailwind CSS
- ์์ด์ฝ: Heroicons ๋๋ Lucide React
- ํผ: React Hook Form (์ ํ)
๐ฆ ์ต์ข ๊ฒฐ๊ณผ๋ฌผ
์ฌ์ฉ์๋ ๋ค์ํ ์จ๋ผ์ธ ์ฝ์ค๋ฅผ ํ์ํ๊ณ , ๊ฐ ์ฝ์ค์ ๋ฆฌ๋ทฐ๋ฅผ ์ฝ์ผ๋ฉฐ, ์ง์ ์๊ฐ ํ๊ธฐ๋ฅผ ๋จ๊ธธ ์ ์์ต๋๋ค. ๊ฐ์ฌ๋ ์์ ์ ํ์ ๊ณผ ๋ฆฌ๋ทฐ๋ฅผ ํ์ธํ๊ณ , ํ์๋ค์ ํผ๋๋ฐฑ์ ์๋ตํ ์ ์์ต๋๋ค. ์ค์ Udemy, Coursera ๊ฐ์ ํ๋ซํผ์ ๋ฆฌ๋ทฐ ์์คํ ์ ๊ฐ์ํํ ๋ฒ์ ์ ๋๋ค.
์ ์ฝ ์กฐ๊ฑด
- โข Laravel 13.x + Inertia.js + React 19 ๋ชจ๋ ธ๋ฆฌ์ค ๊ตฌ์กฐ (๋ณ๋ API ๋ผ์ฐํธ ์์)
- โข npx create-laravel ๋๋ Laravel Breeze (React + Inertia) starter kit ์ฌ์ฉ
- โข SQLite ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฌ์ฉ (๋ง์ด๊ทธ๋ ์ด์ ํ์)
- โข Tailwind CSS๋ก ๋ฐ์ํ ๋์์ธ ๊ตฌํ
- โข ๋ณ์ ์ 1~5์ ์ ์ ๊ฐ์ผ๋ก ์ ์ฅ
- โข ํ๊ท ๋ณ์ ์ ์์์ ์ฒซ์งธ ์๋ฆฌ๊น์ง ํ์
- โข ๋ฆฌ๋ทฐ ์์ฑ ์ ๋ณ์ ๊ณผ ํ๊ธฐ ๋ด์ฉ ํ์ ์ ๋ ฅ
- โข ์ฝ๋ ์ฃผ์์ผ๋ก ์ฃผ์ ๋ก์ง ์ค๋ช ํฌํจ
- โข ์ค์๊ฐ ์ฑํ ๊ธฐ๋ฅ ๊ธ์ง (HTTP ๊ธฐ๋ฐ ๋๊ธ๋ง ์ฌ์ฉ)
ํ๋กฌํํธ ๋ ์ํผ
์๋ ํ๋กฌํํธ๋ฅผ ์์๋๋ก AI์๊ฒ ๋ณด๋ด๋ฉด ๋ฉ๋๋ค. ๋ณต์ฌ ๋ฒํผ์ ๋๋ฌ ๋ฐ๋ก ์ฌ์ฉํ์ธ์.
mkdir course-review-rating-platform && cd course-review-rating-platform ์ด ํด๋์์ Laravel 13.x + Inertia.js + React 19 ํ๋ก์ ํธ๋ฅผ ์์ํฉ๋๋ค. ๋ค์ ๋ช ๋ น์ผ๋ก Laravel Breeze (React + Inertia) starter kit์ ์ค์นํ์ธ์: ```bash composer create-project laravel/laravel . composer require laravel/breeze --dev php artisan breeze:install react npm install npm run build ``` ํ๋ก์ ํธ ์ด๊ธฐ ์ค์ : 1. `.env` ํ์ผ์์ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ SQLite๋ก ์ค์ (`DB_CONNECTION=sqlite`, `database/database.sqlite` ํ์ผ ์์ฑ) 2. `php artisan migrate` ์คํํ์ฌ ๊ธฐ๋ณธ ํ ์ด๋ธ ์์ฑ 3. `npm run dev` ์คํํ์ฌ Vite ๊ฐ๋ฐ ์๋ฒ ์์ 4. `php artisan serve` ์คํํ์ฌ Laravel ์๋ฒ ์์ ๊ธฐ๋ณธ ๊ตฌ์กฐ๊ฐ ์ ์ ์๋ํ๋์ง ํ์ธํ์ธ์ (Welcome ํ์ด์ง ๋๋ Breeze ๋ก๊ทธ์ธ ํ์ด์ง ์ ์).
๊ธฐ๋ ๊ฒฐ๊ณผ: Laravel 13.x + Breeze (React + Inertia) ํ๋ก์ ํธ๊ฐ ์์ฑ๋๊ณ , ๊ธฐ๋ณธ ํ์ด์ง๊ฐ ๋ธ๋ผ์ฐ์ ์์ ์ ์ ๋ ๋๋ง๋ฉ๋๋ค. SQLite ๋ฐ์ดํฐ๋ฒ ์ด์ค๊ฐ ์ค์ ๋๊ณ , Vite + Laravel ์๋ฒ๊ฐ ์คํ๋ฉ๋๋ค.
๋ฐ์ดํฐ๋ฒ ์ด์ค ์คํค๋ง๋ฅผ ์ค๊ณํ๊ณ ๋ง์ด๊ทธ๋ ์ด์ ์ ์์ฑํ์ธ์. ๋ค์ ์ธ ๊ฐ์ ๋ชจ๋ธ๊ณผ ๋ง์ด๊ทธ๋ ์ด์ ์ด ํ์ํฉ๋๋ค: 1. **Instructor (๊ฐ์ฌ)** - `id` (Primary Key) - `name` (๊ฐ์ฌ ์ด๋ฆ, string) - `bio` (์๊ฐ, text) - `profile_image` (ํ๋กํ ์ด๋ฏธ์ง URL, string, nullable) - `timestamps` 2. **Course (์ฝ์ค)** - `id` (Primary Key) - `instructor_id` (Foreign Key โ instructors.id) - `title` (์ฝ์ค ์ ๋ชฉ, string) - `description` (์ฝ์ค ์ค๋ช , text) - `thumbnail` (์ธ๋ค์ผ ์ด๋ฏธ์ง URL, string, nullable) - `price` (๊ฐ๊ฒฉ, decimal 8,2, nullable) - `timestamps` 3. **Review (๋ฆฌ๋ทฐ)** - `id` (Primary Key) - `course_id` (Foreign Key โ courses.id) - `user_id` (Foreign Key โ users.id, Breeze ๊ธฐ๋ณธ User ๋ชจ๋ธ ํ์ฉ) - `rating` (๋ณ์ , integer, 1~5) - `comment` (ํ๊ธฐ ๋ด์ฉ, text) - `is_recommended` (์ถ์ฒ ์ฌ๋ถ, boolean, default false) - `helpful_count` (๋์๋จ ํฌํ ์, integer, default 0) - `timestamps` ๋ง์ด๊ทธ๋ ์ด์ ์์ฑ ๋ช ๋ น: ```bash php artisan make:model Instructor -m php artisan make:model Course -m php artisan make:model Review -m ``` ๊ฐ ๋ง์ด๊ทธ๋ ์ด์ ํ์ผ์ ์ ์คํค๋ง๋ฅผ ์์ฑํ๊ณ , `php artisan migrate`๋ฅผ ์คํํ์ธ์. ๋ชจ๋ธ ํ์ผ (`app/Models/`)์ Eloquent ๊ด๊ณ๋ฅผ ์ ์ํ์ธ์: - `Instructor` hasMany `Course` - `Course` belongsTo `Instructor`, hasMany `Review` - `Review` belongsTo `Course`, belongsTo `User` ์๋(Seeder)๋ฅผ ๋ง๋ค์ด ์ํ ๋ฐ์ดํฐ๋ฅผ ์์ฑํ์ธ์ (๊ฐ์ฌ 3๋ช , ์ฝ์ค 10๊ฐ, ๋ฆฌ๋ทฐ 30๊ฐ ์ ๋): ```bash php artisan make:seeder DatabaseSeeder ``` `DatabaseSeeder.php`์์ ํฉํ ๋ฆฌ ๋๋ ์๋์ผ๋ก ์ํ ๋ฐ์ดํฐ๋ฅผ ์์ฑํ๊ณ , `php artisan db:seed` ์คํ.
๊ธฐ๋ ๊ฒฐ๊ณผ: ๋ฐ์ดํฐ๋ฒ ์ด์ค์ instructors, courses, reviews ํ ์ด๋ธ์ด ์์ฑ๋๊ณ , Eloquent ๊ด๊ณ๊ฐ ์ ์๋ฉ๋๋ค. ์๋ ์คํ ํ ์ํ ๋ฐ์ดํฐ๊ฐ DB์ ์ ์ฅ๋ฉ๋๋ค.
์ฝ์ค ๋ชฉ๋ก ํ์ด์ง๋ฅผ ๊ตฌํํ์ธ์.
Laravel ์ปจํธ๋กค๋ฌ์ Inertia ํ์ด์ง ์ปดํฌ๋ํธ๋ฅผ ์์ฑํฉ๋๋ค.
1. **์ปจํธ๋กค๋ฌ ์์ฑ:**
```bash
php artisan make:controller CourseController
```
`CourseController`์ `index` ๋ฉ์๋๋ฅผ ์์ฑํ์ธ์:
- ๋ชจ๋ ์ฝ์ค๋ฅผ ์กฐํํ๊ณ , ๊ฐ ์ฝ์ค์ ํ๊ท ๋ณ์ (`reviews`์ `rating` ํ๊ท )๊ณผ ๋ฆฌ๋ทฐ ๊ฐ์๋ฅผ ๊ณ์ฐ
- `withAvg('reviews', 'rating')`, `withCount('reviews')` ์ฌ์ฉ
- Instructor ์ ๋ณด๋ ํจ๊ป ๋ก๋ (`with('instructor')`)
- Inertia::render()๋ก 'Courses/Index' ํ์ด์ง์ ๋ฐ์ดํฐ ์ ๋ฌ
2. **๋ผ์ฐํธ ๋ฑ๋ก (`routes/web.php`):**
```php
use App\Http\Controllers\CourseController;
Route::get('/courses', [CourseController::class, 'index'])->name('courses.index');
```
3. **React ํ์ด์ง ์ปดํฌ๋ํธ ์์ฑ (`resources/js/Pages/Courses/Index.jsx`):**
- Inertia์ `usePage()` ํ
์ผ๋ก ์ ๋ฌ๋ฐ์ courses ๋ฐ์ดํฐ ์ฌ์ฉ
- ์ฝ์ค ๋ชฉ๋ก์ ๊ทธ๋ฆฌ๋ ๋ ์ด์์(Tailwind CSS grid)์ผ๋ก ํ์
- ๊ฐ ์ฝ์ค ์นด๋์ ์ธ๋ค์ผ, ์ ๋ชฉ, ๊ฐ์ฌ ์ด๋ฆ, ํ๊ท ๋ณ์ (โญ ์์ด์ฝ), ๋ฆฌ๋ทฐ ์ ํ์
- ๋ณ์ ์ ์ซ์ ๋๋ ๋ณ ์์ด์ฝ์ผ๋ก ์๊ฐํ (์: 4.5 โ โญโญโญโญโ)
- ์ฝ์ค ์นด๋ ํด๋ฆญ ์ ์์ธ ํ์ด์ง๋ก ์ด๋ (`/courses/{id}`)
Tailwind CSS๋ก ๋ฐ์ํ ๋์์ธ ์ ์ฉ (๋ชจ๋ฐ์ผ: 1์ด, ํ๋ธ๋ฆฟ: 2์ด, ๋ฐ์คํฌํฑ: 3์ด).
๋ธ๋ผ์ฐ์ ์์ `/courses` ์ ์ํ์ฌ ์ฝ์ค ๋ชฉ๋ก์ด ์ ์ ํ์๋๋์ง ํ์ธํ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: ์ฝ์ค ๋ชฉ๋ก ํ์ด์ง๊ฐ ๋ ๋๋ง๋๊ณ , ๊ฐ ์ฝ์ค์ ์ธ๋ค์ผ, ์ ๋ชฉ, ๊ฐ์ฌ ์ด๋ฆ, ํ๊ท ๋ณ์ , ๋ฆฌ๋ทฐ ์๊ฐ ์นด๋ ํํ๋ก ๊ทธ๋ฆฌ๋์ ํ์๋ฉ๋๋ค. ๋ฐ์ํ ๋ ์ด์์์ด ์ ์ฉ๋ฉ๋๋ค.
์ฝ์ค ์์ธ ํ์ด์ง๋ฅผ ๊ตฌํํ์ธ์.
1. **์ปจํธ๋กค๋ฌ ๋ฉ์๋ ์ถ๊ฐ (`CourseController::show`):**
- ํน์ ์ฝ์ค ID๋ก ์ฝ์ค ์กฐํ
- Instructor, Reviews(+User ์ ๋ณด) eager loading
- ํ๊ท ๋ณ์ , ๋ฆฌ๋ทฐ ๊ฐ์, ๋ณ์ ๋ถํฌ(1~5์ ๋ณ ๊ฐ์) ๊ณ์ฐ
- Inertia::render()๋ก 'Courses/Show' ํ์ด์ง์ ๋ฐ์ดํฐ ์ ๋ฌ
2. **๋ผ์ฐํธ ์ถ๊ฐ:**
```php
Route::get('/courses/{course}', [CourseController::class, 'show'])->name('courses.show');
```
3. **React ํ์ด์ง ์ปดํฌ๋ํธ (`resources/js/Pages/Courses/Show.jsx`):**
- ์ฝ์ค ์ ๋ณด ์น์
: ์ธ๋ค์ผ(ํฐ ์ด๋ฏธ์ง), ์ ๋ชฉ, ๊ฐ์ฌ ์ด๋ฆ(ํด๋ฆญ ์ ๊ฐ์ฌ ํ๋กํ๋ก ์ด๋), ์ค๋ช
, ๊ฐ๊ฒฉ
- ํ๊ท ๋ณ์ ๋ฐ ๋ฆฌ๋ทฐ ํต๊ณ ์น์
:
- ํฐ ๋ณ์ ์ซ์ (์: 4.3 / 5.0)
- ๋ณ์ ๋ถํฌ ๋ฐ ์ฐจํธ (1~5์ ๋ณ ๋ฆฌ๋ทฐ ๊ฐ์๋ฅผ ๊ฐ๋ก ๋ง๋๋ก ํ์)
- ์ด ๋ฆฌ๋ทฐ ๊ฐ์
- ๋ฆฌ๋ทฐ ๋ชฉ๋ก ์น์
:
- ๊ฐ ๋ฆฌ๋ทฐ: ์์ฑ์ ์ด๋ฆ, ๋ณ์ , ์์ฑ์ผ, ํ๊ธฐ ๋ด์ฉ, ์ถ์ฒ ์ฌ๋ถ ๋ฐฐ์ง
- ์ต์ ์ ์ ๋ ฌ (๋์ค์ ํํฐ ์ถ๊ฐ)
- "๋ฆฌ๋ทฐ ์์ฑํ๊ธฐ" ๋ฒํผ (ํด๋ฆญ ์ ๋ฆฌ๋ทฐ ์์ฑ ํผ์ผ๋ก ์ด๋ ๋๋ ๋ชจ๋ฌ ์ด๊ธฐ)
Tailwind CSS๋ก ๋ ์ด์์ ๊ตฌ์ฑ (2์ด ๋ ์ด์์: ์ผ์ชฝ ์ฝ์ค ์ ๋ณด, ์ค๋ฅธ์ชฝ ๋ฆฌ๋ทฐ ๋ชฉ๋ก).
๋ธ๋ผ์ฐ์ ์์ ์ฝ์ค ์นด๋ ํด๋ฆญ โ ์์ธ ํ์ด์ง ์ด๋ ํ์ธ.
๊ธฐ๋ ๊ฒฐ๊ณผ: ์ฝ์ค ์์ธ ํ์ด์ง๊ฐ ๋ ๋๋ง๋๊ณ , ์ฝ์ค ์ ๋ณด, ํ๊ท ๋ณ์ , ๋ณ์ ๋ถํฌ ์ฐจํธ, ๋ฆฌ๋ทฐ ๋ชฉ๋ก์ด ํ์๋ฉ๋๋ค. ๋ฆฌ๋ทฐ ์์ฑ ๋ฒํผ์ด ๋ณด์ ๋๋ค.
๋ฆฌ๋ทฐ ์์ฑ ๊ธฐ๋ฅ์ ๊ตฌํํ์ธ์.
1. **๋ฆฌ๋ทฐ ์์ฑ ํผ ์ปดํฌ๋ํธ (`resources/js/Pages/Reviews/Create.jsx` ๋๋ ๋ชจ๋ฌ ์ปดํฌ๋ํธ):**
- ๋ณ์ ์ ํ UI (1~5 ๋ณ ์์ด์ฝ ํด๋ฆญ ๋๋ ๋ผ๋์ค ๋ฒํผ)
- ํ๊ธฐ ๋ด์ฉ ํ
์คํธ ์์ญ (ํ์, ์ต์ 10์)
- ์ถ์ฒ ์ฌ๋ถ ์ฒดํฌ๋ฐ์ค ("์ด ์ฝ์ค๋ฅผ ์ถ์ฒํ์๋์?")
- ์ ์ถ ๋ฒํผ
2. **์ปจํธ๋กค๋ฌ ๋ฉ์๋ (`ReviewController`):**
```bash
php artisan make:controller ReviewController
```
- `create` ๋ฉ์๋: ๋ฆฌ๋ทฐ ์์ฑ ํผ ํ์ด์ง ๋ ๋๋ง (course_id๋ฅผ URL ํ๋ผ๋ฏธํฐ๋ก ์ ๋ฌ)
- `store` ๋ฉ์๋:
- ํผ ๋ฐ์ดํฐ ๊ฒ์ฆ (rating: required|integer|between:1,5, comment: required|min:10)
- ํ์ฌ ๋ก๊ทธ์ธ ์ ์ ID (Breeze ์ธ์ฆ ์ฌ์ฉ, `auth()->id()`)
- Review ๋ชจ๋ธ์ ๋ฐ์ดํฐ ์ ์ฅ
- ์ ์ฅ ํ ์ฝ์ค ์์ธ ํ์ด์ง๋ก ๋ฆฌ๋ค์ด๋ ํธ (`redirect()->route('courses.show', $course)`)
3. **๋ผ์ฐํธ ์ถ๊ฐ:**
```php
Route::middleware('auth')->group(function () {
Route::get('/courses/{course}/reviews/create', [ReviewController::class, 'create'])->name('reviews.create');
Route::post('/courses/{course}/reviews', [ReviewController::class, 'store'])->name('reviews.store');
});
```
4. **์ฝ์ค ์์ธ ํ์ด์ง์์ "๋ฆฌ๋ทฐ ์์ฑํ๊ธฐ" ๋ฒํผ ์ฐ๊ฒฐ:**
- `<Link href={route('reviews.create', course.id)}>๋ฆฌ๋ทฐ ์์ฑ</Link>` (Inertia Link ์ปดํฌ๋ํธ ์ฌ์ฉ)
- ๋ก๊ทธ์ธํ์ง ์์ ๊ฒฝ์ฐ ๋ก๊ทธ์ธ ํ์ด์ง๋ก ๋ฆฌ๋ค์ด๋ ํธ (๋ฏธ๋ค์จ์ด ์ฒ๋ฆฌ)
5. **ํผ ์ ์ถ ์ฒ๋ฆฌ (React):**
- Inertia์ `useForm()` ํ
์ฌ์ฉ
- ํผ ๊ฒ์ฆ ์๋ฌ ํ์ (Laravel ๊ฒ์ฆ ์๋ฌ๋ฅผ Inertia๊ฐ ์๋ ์ ๋ฌ)
๋ฆฌ๋ทฐ ์์ฑ ํ ์ฝ์ค ์์ธ ํ์ด์ง์์ ์ ๋ฆฌ๋ทฐ๊ฐ ๋ชฉ๋ก์ ์ถ๊ฐ๋์๋์ง ํ์ธํ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: ๋ก๊ทธ์ธํ ์ฌ์ฉ์๊ฐ ์ฝ์ค ์์ธ ํ์ด์ง์์ "๋ฆฌ๋ทฐ ์์ฑ" ๋ฒํผ์ ํด๋ฆญํ๋ฉด ๋ฆฌ๋ทฐ ์์ฑ ํผ์ด ์ด๋ฆฝ๋๋ค. ๋ณ์ , ํ๊ธฐ ๋ด์ฉ, ์ถ์ฒ ์ฌ๋ถ๋ฅผ ์ ๋ ฅํ๊ณ ์ ์ถํ๋ฉด ๋ฆฌ๋ทฐ๊ฐ DB์ ์ ์ฅ๋๊ณ , ์ฝ์ค ์์ธ ํ์ด์ง๋ก ๋์๊ฐ ์ ๋ฆฌ๋ทฐ๊ฐ ํ์๋ฉ๋๋ค.
๋ฆฌ๋ทฐ ํํฐ๋ง ๋ฐ ์ ๋ ฌ ๊ธฐ๋ฅ์ ์ถ๊ฐํ์ธ์.
1. **์ฝ์ค ์์ธ ํ์ด์ง์ ํํฐ UI ์ถ๊ฐ (`Courses/Show.jsx`):**
- ๋ณ์ ํํฐ ๋ฒํผ (์ ์ฒด, 5์ , 4์ , 3์ , 2์ , 1์ )
- ์ ๋ ฌ ๋๋กญ๋ค์ด (์ต์ ์, ์ถ์ฒ์, ๋ณ์ ๋์ ์, ๋ณ์ ๋ฎ์ ์)
2. **ํํฐ/์ ๋ ฌ ์ํ ๊ด๋ฆฌ (React useState):**
- `selectedRating` (null ๋๋ 1~5)
- `sortBy` ('latest', 'recommended', 'rating_high', 'rating_low')
3. **ํํฐ ๋ณ๊ฒฝ ์ Laravel ์ปจํธ๋กค๋ฌ์ ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ ์ ๋ฌ:**
- Inertia Link ๋๋ useForm์ผ๋ก GET ์์ฒญ (`/courses/{id}?rating=5&sort=latest`)
- ์ปจํธ๋กค๋ฌ์์ `request()->query('rating')`, `request()->query('sort')` ์ฝ๊ธฐ
- ๋ฆฌ๋ทฐ ์กฐํ ์ ์กฐ๊ฑด ์ ์ฉ:
- rating ํํฐ: `$query->where('rating', $rating)`
- sort: `$query->orderBy('created_at', 'desc')` (์ต์ ์), `$query->orderBy('rating', 'desc')` (๋ณ์ ๋์ ์) ๋ฑ
4. **ํํฐ ์ ์ฉ ํ ๋ฆฌ๋ทฐ ๋ชฉ๋ก ์
๋ฐ์ดํธ:**
- Inertia๊ฐ ์๋์ผ๋ก ํ์ด์ง๋ฅผ ๋ค์ ๋ ๋๋งํ๋ฉฐ ํํฐ๋ ๋ฆฌ๋ทฐ ํ์
5. **UI ๊ฐ์ :**
- ํ์ฌ ์ ํ๋ ํํฐ ๋ฒํผ ๊ฐ์กฐ (Tailwind CSS ์กฐ๊ฑด๋ถ ํด๋์ค)
- ํํฐ ์ด๊ธฐํ ๋ฒํผ ์ถ๊ฐ ("์ ์ฒด ๋ณด๊ธฐ")
๋ธ๋ผ์ฐ์ ์์ ๋ณ์ ํํฐ ํด๋ฆญ โ ํด๋น ๋ณ์ ์ ๋ฆฌ๋ทฐ๋ง ํ์๋๋์ง ํ์ธํ์ธ์.
๊ธฐ๋ ๊ฒฐ๊ณผ: ์ฝ์ค ์์ธ ํ์ด์ง์์ ๋ณ์ ํํฐ ๋ฒํผ๊ณผ ์ ๋ ฌ ๋๋กญ๋ค์ด์ด ์๋ํ๋ฉฐ, ์ ํํ ์กฐ๊ฑด์ ๋ฐ๋ผ ๋ฆฌ๋ทฐ ๋ชฉ๋ก์ด ๋์ ์ผ๋ก ์ ๋ฐ์ดํธ๋ฉ๋๋ค. URL ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ๊ฐ ๋ณ๊ฒฝ๋ฉ๋๋ค.
๊ฐ์ฌ ํ๋กํ ํ์ด์ง๋ฅผ ๊ตฌํํ์ธ์.
1. **์ปจํธ๋กค๋ฌ ์์ฑ:**
```bash
php artisan make:controller InstructorController
```
`InstructorController::show` ๋ฉ์๋:
- ํน์ ๊ฐ์ฌ ID๋ก ๊ฐ์ฌ ์กฐํ
- ๊ฐ์ฌ๊ฐ ๋ด๋นํ๋ ์ฝ์ค ๋ชฉ๋ก ์กฐํ (`with('courses')`)
- ๊ฐ์ฌ์ ์ ์ฒด ํ๊ท ํ์ ๊ณ์ฐ (๋ชจ๋ ์ฝ์ค์ ๋ฆฌ๋ทฐ ํ๊ท )
- Inertia::render()๋ก 'Instructors/Show' ํ์ด์ง์ ๋ฐ์ดํฐ ์ ๋ฌ
2. **๋ผ์ฐํธ ์ถ๊ฐ:**
```php
Route::get('/instructors/{instructor}', [InstructorController::class, 'show'])->name('instructors.show');
```
3. **React ํ์ด์ง ์ปดํฌ๋ํธ (`resources/js/Pages/Instructors/Show.jsx`):**
- ๊ฐ์ฌ ํ๋กํ ์น์
:
- ํ๋กํ ์ด๋ฏธ์ง (์ํ ๋๋ ์ ์ฌ๊ฐํ)
- ๊ฐ์ฌ ์ด๋ฆ, ์๊ฐ(bio)
- ์ ์ฒด ํ๊ท ํ์ (โญ ์์ด์ฝ)
- ๋ด๋น ์ฝ์ค ๋ชฉ๋ก ์น์
:
- ์ฝ์ค ์นด๋ ๊ทธ๋ฆฌ๋ (์ฝ์ค ๋ชฉ๋ก ํ์ด์ง์ ๋์ผํ ์คํ์ผ)
- ๊ฐ ์ฝ์ค ์นด๋ ํด๋ฆญ ์ ์ฝ์ค ์์ธ ํ์ด์ง๋ก ์ด๋
4. **์ฝ์ค ์์ธ ํ์ด์ง์์ ๊ฐ์ฌ ์ด๋ฆ ํด๋ฆญ ์ ๊ฐ์ฌ ํ๋กํ๋ก ์ด๋:**
- `<Link href={route('instructors.show', course.instructor.id)}>{course.instructor.name}</Link>`
Tailwind CSS๋ก ํ๋กํ ๋ ์ด์์ ๊ตฌ์ฑ (์๋จ: ํ๋กํ ์ ๋ณด, ํ๋จ: ์ฝ์ค ๊ทธ๋ฆฌ๋).
๋ธ๋ผ์ฐ์ ์์ ์ฝ์ค ์์ธ โ ๊ฐ์ฌ ์ด๋ฆ ํด๋ฆญ โ ๊ฐ์ฌ ํ๋กํ ํ์ด์ง ํ์ธ.
๊ธฐ๋ ๊ฒฐ๊ณผ: ๊ฐ์ฌ ํ๋กํ ํ์ด์ง๊ฐ ๋ ๋๋ง๋๊ณ , ๊ฐ์ฌ ์ ๋ณด, ์ ์ฒด ํ๊ท ํ์ , ๋ด๋น ์ฝ์ค ๋ชฉ๋ก์ด ํ์๋ฉ๋๋ค. ์ฝ์ค ์นด๋ ํด๋ฆญ ์ ์ฝ์ค ์์ธ ํ์ด์ง๋ก ์ด๋ํฉ๋๋ค.
๋ณด๋์ค ๊ธฐ๋ฅ: ๋ฆฌ๋ทฐ "๋์๋จ" ํฌํ ๊ธฐ๋ฅ์ ์ถ๊ฐํ์ธ์.
1. **๋ฆฌ๋ทฐ ํ
์ด๋ธ์ `helpful_count` ์ปฌ๋ผ์ด ์ด๋ฏธ ์๋์ง ํ์ธ (step 2์์ ์ถ๊ฐ).**
2. **์ปจํธ๋กค๋ฌ ๋ฉ์๋ (`ReviewController::helpful`):**
- ํน์ ๋ฆฌ๋ทฐ์ `helpful_count`๋ฅผ 1 ์ฆ๊ฐ
- `$review->increment('helpful_count');`
- JSON ์๋ต์ผ๋ก ์
๋ฐ์ดํธ๋ ์นด์ดํธ ๋ฐํ (๋๋ Inertia ๋ฆฌ๋ค์ด๋ ํธ)
3. **๋ผ์ฐํธ ์ถ๊ฐ:**
```php
Route::post('/reviews/{review}/helpful', [ReviewController::class, 'helpful'])->name('reviews.helpful');
```
4. **์ฝ์ค ์์ธ ํ์ด์ง ๋ฆฌ๋ทฐ ๋ชฉ๋ก์ "๋์๋จ" ๋ฒํผ ์ถ๊ฐ (`Courses/Show.jsx`):**
- ๊ฐ ๋ฆฌ๋ทฐ ์นด๋์ ๐ ๋ฒํผ๊ณผ ์นด์ดํธ ํ์
- ๋ฒํผ ํด๋ฆญ ์ Inertia `post` ์์ฒญ์ผ๋ก `/reviews/{review.id}/helpful` ํธ์ถ
- ์ฑ๊ณต ํ ํ์ด์ง ์๋ก๊ณ ์นจํ์ฌ ์นด์ดํธ ์
๋ฐ์ดํธ (๋๋ React ์ํ๋ก ์ฆ์ ๋ฐ์)
5. **๋ฒ ์คํธ ๋ฆฌ๋ทฐ ๋ฐฐ์ง ์ถ๊ฐ:**
- `helpful_count >= 10`์ธ ๋ฆฌ๋ทฐ์ "๐ ๋ฒ ์คํธ ๋ฆฌ๋ทฐ" ๋ฐฐ์ง ํ์ (Tailwind CSS ๋ฐฐ์ง ์คํ์ผ)
6. **์ ๋ ฌ ์ต์
์ "์ถ์ฒ์" ์ถ๊ฐ:**
- ์ปจํธ๋กค๋ฌ์์ `orderBy('helpful_count', 'desc')` ์ ์ฉ
๋ธ๋ผ์ฐ์ ์์ ๋ฆฌ๋ทฐ "๋์๋จ" ๋ฒํผ ํด๋ฆญ โ ์นด์ดํธ ์ฆ๊ฐ ํ์ธ, 10๊ฐ ์ด์ ์ ๋ฐฐ์ง ํ์ ํ์ธ.
๊ธฐ๋ ๊ฒฐ๊ณผ: ๋ฆฌ๋ทฐ ๋ชฉ๋ก์ "๋์๋จ" ๋ฒํผ์ด ์ถ๊ฐ๋๊ณ , ํด๋ฆญ ์ ์นด์ดํธ๊ฐ ์ฆ๊ฐํฉ๋๋ค. ์นด์ดํธ๊ฐ 10 ์ด์์ธ ๋ฆฌ๋ทฐ์ ๋ฒ ์คํธ ๋ฆฌ๋ทฐ ๋ฐฐ์ง๊ฐ ํ์๋ฉ๋๋ค. ์ ๋ ฌ ์ต์ ์์ ์ถ์ฒ์ ์ ํ ์ ๋์๋จ ์๊ฐ ๋ง์ ๋ฆฌ๋ทฐ๊ฐ ์๋จ์ ํ์๋ฉ๋๋ค.
์ ์ถ๋ ์ํ (0)
๋ก๊ทธ์ธ ํ ์ ์ถ์์ง ์ ์ถ๋ ์ํ์ด ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ ๋์ ์๊ฐ ๋์ด๋ณด์ธ์!