크리에이터 디지털 굿즈 스토어 - 작품 판매 & 구매자 리뷰
creator-digital-goods-store
문제 설명
🎨 크리에이터를 위한 디지털 굿즈 판매 플랫폼
일러스트레이터, 디자이너, 작가 등 크리에이터가 자신의 디지털 작품(이미지, PDF, 폰트, 템플릿 등)을 업로드하고 판매할 수 있는 미니 마켓플레이스를 만듭니다. Laravel 13.x + Inertia.js + React 19 모노리스 풀스택 구조로, 상품 등록부터 구매 후 다운로드, 리뷰 작성까지 전체 플로우를 구현합니다.
필수 기능
- 크리에이터 대시보드: 내 상품 목록, 판매 통계 (총 판매 수, 수익 합계) 확인
- 상품 등록/수정: 제목, 설명, 카테고리, 가격, 썸네일 이미지, 디지털 파일 업로드 (Laravel Storage 사용)
- 상품 목록 페이지: 카테고리 필터링, 가격 범위 필터, 최신순/인기순 정렬, 검색 기능
- 상품 상세 페이지: 이미지 갤러리, 상세 설명, 구매 버튼, 리뷰 목록 (별점 평균 표시)
- 장바구니: 여러 상품 담기, 수량 조정 (디지털 상품이지만 여러 개 구매 가능), 총액 계산
- 주문/결제 시뮬레이션: 실제 결제 API 대신 '결제 완료' 버튼으로 주문 생성 (orders 테이블에 기록)
- 구매 내역 & 다운로드: 사용자가 구매한 상품 목록 조회, 다운로드 버튼 (파일 다운로드 라우트)
- 리뷰 작성: 구매한 상품에 한해 별점(1~5) + 텍스트 리뷰 작성, 수정/삭제 기능
- 인증 시스템: Laravel Breeze (React + Inertia) 기본 인증 활용, 로그인/회원가입/로그아웃
보너스 기능
- 좋아요(찜) 기능: 상품에 하트 버튼, 내가 찜한 상품 목록 페이지
- 크리에이터 프로필 페이지: 크리에이터별 상품 목록, 소개, 팔로워 수 (간단한 follow 테이블)
- 태그 시스템: 상품에 태그 추가, 태그 클릭 시 관련 상품 필터링
- 쿠폰/할인 코드: 주문 시 쿠폰 코드 입력하면 할인 적용 (coupons 테이블)
- 상품 미리보기 이미지 슬라이더: React 라이브러리(swiper 등) 활용한 이미지 갤러리
기술 스택
- 백엔드: Laravel 13.x (Eloquent ORM, 마이그레이션, 파일 업로드/다운로드)
- 프론트엔드: React 19 + Inertia.js (모노리스 풀스택, 별도 API 없음)
- 스타일링: Tailwind CSS
- DB: SQLite (개발 환경, 배포 시 MySQL/PostgreSQL 전환 가능)
- 인증: Laravel Breeze (React + Inertia starter kit)
학습 목표
- Laravel 컨트롤러에서 Inertia::render()로 React 컴포넌트에 데이터 전달하는 방법
- Eloquent 관계 설정 (User hasMany Products, Product hasMany Reviews 등)
- 파일 업로드/다운로드 처리 (Laravel Storage 퍼블릭 디스크)
- React에서 폼 제출, 상태 관리(useState), 컴포넌트 분리
- 다대다 관계 (User와 Product 간 좋아요/찜 기능: pivot 테이블)
- 필터링/정렬/검색 쿼리 구현 (Eloquent where, orderBy)
- 주문/결제 플로우 설계 (간단한 e-commerce 로직)
제약 조건
- • Laravel 13.x + Inertia.js + React 19 모노리스 풀스택 (별도 API 라우트 없음)
- • Laravel Breeze (React + Inertia) starter kit으로 프로젝트 시작
- • SQLite 데이터베이스 사용 (database/database.sqlite)
- • Tailwind CSS로 스타일링 (Breeze 기본 제공)
- • 파일 업로드는 Laravel Storage 퍼블릭 디스크 사용 (php artisan storage:link)
- • 실제 결제 API 연동 없이 '결제 완료' 버튼으로 주문 생성 시뮬레이션
- • 디지털 파일 다운로드는 Laravel의 response()->download() 활용
- • 모든 CRUD 작업은 Laravel 컨트롤러 메서드에서 처리, Inertia::render()로 React 페이지에 전달
- • React 컴포넌트는 resources/js/Pages 폴더에 구성 (Breeze 구조 따름)
- • 인증 미들웨어 활용: 로그인한 사용자만 상품 등록/구매/리뷰 작성 가능
- • 최소 5개 이상의 페이지 컴포넌트 (Home, ProductList, ProductDetail, Cart, MyPurchases, CreatorDashboard 등)
프롬프트 레시피
아래 프롬프트를 순서대로 AI에게 보내면 됩니다. 복사 버튼을 눌러 바로 사용하세요.
mkdir creator-digital-goods-store && cd creator-digital-goods-store 이 폴더에서 Laravel 13.x + Inertia.js + React 19 프로젝트를 시작합니다. 1. Laravel Breeze (React + Inertia) starter kit으로 새 프로젝트를 생성하세요. - 명령어: `composer create-project laravel/laravel . "13.*"` - Laravel Breeze 설치: `composer require laravel/breeze --dev` - Breeze React + Inertia 스택 설치: `php artisan breeze:install react` - 의존성 설치: `npm install && npm run build` 2. SQLite 데이터베이스 설정 - .env 파일에서 DB_CONNECTION=sqlite 설정 - database/database.sqlite 파일 생성: `touch database/database.sqlite` - 마이그레이션 실행: `php artisan migrate` 3. 스토리지 심볼릭 링크 생성 (파일 업로드용) - `php artisan storage:link` 4. 개발 서버 실행 - `php artisan serve` (백엔드) - `npm run dev` (프론트엔드, 별도 터미널) 5. 기본 인증 확인 - 브라우저에서 http://localhost:8000 접속 - 회원가입/로그인 페이지가 정상 작동하는지 확인 위 단계를 완료하고, Laravel Breeze (React + Inertia) 프로젝트가 정상 실행되는지 확인해주세요. 기본 Welcome 페이지와 Login/Register 페이지가 보이면 성공입니다.
기대 결과: Laravel 13.x + Breeze (React + Inertia) 프로젝트 생성 완료. SQLite DB 설정, 마이그레이션 완료, 스토리지 링크 생성. 개발 서버 실행 시 http://localhost:8000에서 기본 페이지 확인 가능. resources/js/Pages에 Welcome.jsx, Auth/Login.jsx 등 React 컴포넌트 존재.
이제 데이터베이스 구조를 설계합니다. 크리에이터 디지털 굿즈 스토어에 필요한 테이블과 모델을 생성하세요.
1. **Product 모델 및 마이그레이션 생성**
- `php artisan make:model Product -m`
- 마이그레이션 파일(database/migrations/xxxx_create_products_table.php)에 다음 컬럼 추가:
- user_id (크리에이터, foreignId, users 테이블 참조)
- title (string, 상품명)
- description (text, 상세 설명)
- category (string, 카테고리: 'illustration', 'template', 'font', 'ebook' 등)
- price (decimal, 8, 2, 가격)
- thumbnail (string, nullable, 썸네일 이미지 경로)
- file_path (string, 디지털 파일 경로)
- downloads_count (integer, default 0, 다운로드 횟수)
- rating_avg (decimal, 3, 2, nullable, 평균 별점)
- timestamps
2. **Review 모델 및 마이그레이션 생성**
- `php artisan make:model Review -m`
- 마이그레이션 컬럼:
- user_id (리뷰 작성자, foreignId)
- product_id (상품, foreignId)
- rating (tinyInteger, 1~5)
- comment (text, nullable)
- timestamps
3. **Order 모델 및 마이그레이션 생성**
- `php artisan make:model Order -m`
- 마이그레이션 컬럼:
- user_id (구매자, foreignId)
- total_price (decimal, 8, 2)
- status (string, default 'completed', 결제 시뮬레이션이므로 바로 완료)
- timestamps
4. **OrderItem 모델 및 마이그레이션 생성** (주문-상품 다대다 관계)
- `php artisan make:model OrderItem -m`
- 마이그레이션 컬럼:
- order_id (foreignId)
- product_id (foreignId)
- quantity (integer, 수량)
- price (decimal, 8, 2, 구매 당시 가격)
- timestamps
5. **Favorite 모델 및 마이그레이션 생성** (찜 기능, 보너스)
- `php artisan make:model Favorite -m`
- 마이그레이션 컬럼:
- user_id (foreignId)
- product_id (foreignId)
- timestamps
- unique(['user_id', 'product_id']) (중복 방지)
6. 마이그레이션 실행
- `php artisan migrate`
7. 모델 관계 설정 (app/Models 폴더의 각 모델 파일 수정)
- User.php: `public function products() { return $this->hasMany(Product::class); }` (크리에이터가 등록한 상품)
- User.php: `public function orders() { return $this->hasMany(Order::class); }` (구매 내역)
- User.php: `public function favorites() { return $this->belongsToMany(Product::class, 'favorites'); }` (찜한 상품)
- Product.php: `public function user() { return $this->belongsTo(User::class); }` (크리에이터)
- Product.php: `public function reviews() { return $this->hasMany(Review::class); }` (리뷰)
- Product.php: `public function orderItems() { return $this->hasMany(OrderItem::class); }`
- Review.php: `public function user() { return $this->belongsTo(User::class); }`
- Review.php: `public function product() { return $this->belongsTo(Product::class); }`
- Order.php: `public function user() { return $this->belongsTo(User::class); }`
- Order.php: `public function items() { return $this->hasMany(OrderItem::class); }`
- OrderItem.php: `public function order() { return $this->belongsTo(Order::class); }`
- OrderItem.php: `public function product() { return $this->belongsTo(Product::class); }`
8. Product 모델에 fillable 속성 추가 (app/Models/Product.php)
- `protected $fillable = ['user_id', 'title', 'description', 'category', 'price', 'thumbnail', 'file_path', 'downloads_count', 'rating_avg'];`
위 단계를 완료하면 DB 구조와 모델 관계가 완성됩니다.
기대 결과: products, reviews, orders, order_items, favorites 테이블 생성 완료. 각 모델에 Eloquent 관계 메서드 정의 완료. php artisan migrate 실행 시 에러 없이 테이블 생성 확인.
이제 크리에이터 대시보드와 상품 등록 기능을 구현합니다. Laravel 컨트롤러와 React 페이지 컴포넌트를 만들어주세요.
1. **ProductController 생성**
- `php artisan make:controller ProductController`
- app/Http/Controllers/ProductController.php에 다음 메서드 추가:
- `index()`: 크리에이터 대시보드 (내 상품 목록)
```php
public function index()
{
$products = auth()->user()->products()->latest()->get();
$totalSales = auth()->user()->products()->sum('downloads_count');
$totalRevenue = OrderItem::whereIn('product_id', auth()->user()->products->pluck('id'))->sum('price');
return Inertia::render('Creator/Dashboard', [
'products' => $products,
'totalSales' => $totalSales,
'totalRevenue' => $totalRevenue,
]);
}
```
- `create()`: 상품 등록 폼 페이지
```php
public function create()
{
return Inertia::render('Creator/CreateProduct');
}
```
- `store(Request $request)`: 상품 저장 (파일 업로드 포함)
```php
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'description' => 'required|string',
'category' => 'required|string',
'price' => 'required|numeric|min:0',
'thumbnail' => 'nullable|image|max:2048',
'file' => 'required|file|max:10240', // 디지털 파일 (최대 10MB)
]);
$thumbnailPath = $request->file('thumbnail') ? $request->file('thumbnail')->store('thumbnails', 'public') : null;
$filePath = $request->file('file')->store('products', 'public');
auth()->user()->products()->create([
'title' => $validated['title'],
'description' => $validated['description'],
'category' => $validated['category'],
'price' => $validated['price'],
'thumbnail' => $thumbnailPath,
'file_path' => $filePath,
]);
return redirect()->route('creator.dashboard')->with('success', '상품이 등록되었습니다!');
}
```
2. **라우트 설정** (routes/web.php)
- 기존 Breeze 라우트 아래에 추가:
```php
use App\Http\Controllers\ProductController;
Route::middleware('auth')->group(function () {
Route::get('/creator/dashboard', [ProductController::class, 'index'])->name('creator.dashboard');
Route::get('/creator/products/create', [ProductController::class, 'create'])->name('creator.products.create');
Route::post('/creator/products', [ProductController::class, 'store'])->name('creator.products.store');
});
```
3. **React 페이지 컴포넌트 생성**
- resources/js/Pages/Creator/Dashboard.jsx 생성
- props로 products, totalSales, totalRevenue 받음
- 통계 카드 (총 판매 수, 총 수익) 표시
- 내 상품 목록 테이블 (제목, 카테고리, 가격, 다운로드 수, 평균 별점)
- '새 상품 등록' 버튼 (Link to creator.products.create)
- Tailwind CSS로 스타일링 (카드, 테이블 디자인)
- resources/js/Pages/Creator/CreateProduct.jsx 생성
- 상품 등록 폼 (제목, 설명, 카테고리 select, 가격, 썸네일 파일 input, 디지털 파일 input)
- useForm 훅 사용 (Inertia.js 제공): `const { data, setData, post, errors } = useForm({ title: '', description: '', category: 'illustration', price: 0, thumbnail: null, file: null });`
- 폼 제출: `post(route('creator.products.store'))`
- 파일 input 처리: `<input type="file" onChange={e => setData('thumbnail', e.target.files[0])} />`
- 에러 메시지 표시 (errors 객체 활용)
- Tailwind CSS로 폼 스타일링
4. **네비게이션 메뉴에 크리에이터 대시보드 링크 추가**
- resources/js/Layouts/AuthenticatedLayout.jsx 수정
- 상단 네비게이션에 '크리에이터 대시보드' 링크 추가 (Link to creator.dashboard)
위 단계를 완료하고, 로그인 후 크리에이터 대시보드에서 상품을 등록할 수 있는지 테스트하세요. 파일 업로드가 정상 작동하고, storage/app/public/products 폴더에 파일이 저장되는지 확인하세요.
기대 결과: ProductController에 index, create, store 메서드 구현 완료. 라우트 설정 완료. Creator/Dashboard.jsx와 Creator/CreateProduct.jsx 페이지 컴포넌트 생성. 로그인 후 크리에이터 대시보드 접속 가능, 상품 등록 폼 작동 확인. 파일 업로드 시 storage/app/public에 저장.
이제 일반 사용자가 볼 수 있는 상품 목록 페이지와 상품 상세 페이지를 구현합니다. 필터링, 검색, 정렬 기능도 추가하세요.
1. **HomeController 생성** (또는 ProductController에 추가 메서드)
- `php artisan make:controller HomeController`
- app/Http/Controllers/HomeController.php에 다음 메서드 추가:
- `index(Request $request)`: 상품 목록 (메인 페이지)
```php
public function index(Request $request)
{
$query = Product::with('user');
// 카테고리 필터
if ($request->category) {
$query->where('category', $request->category);
}
// 검색
if ($request->search) {
$query->where('title', 'like', '%' . $request->search . '%');
}
// 정렬
$sort = $request->sort ?? 'latest';
if ($sort === 'latest') {
$query->latest();
} elseif ($sort === 'popular') {
$query->orderBy('downloads_count', 'desc');
} elseif ($sort === 'price_low') {
$query->orderBy('price', 'asc');
} elseif ($sort === 'price_high') {
$query->orderBy('price', 'desc');
}
$products = $query->paginate(12);
$categories = ['illustration', 'template', 'font', 'ebook', 'photo', 'music'];
return Inertia::render('Home', [
'products' => $products,
'categories' => $categories,
'filters' => $request->only(['category', 'search', 'sort']),
]);
}
```
- `show($id)`: 상품 상세 페이지
```php
public function show($id)
{
$product = Product::with(['user', 'reviews.user'])->findOrFail($id);
$isFavorited = auth()->check() ? auth()->user()->favorites()->where('product_id', $id)->exists() : false;
return Inertia::render('ProductDetail', [
'product' => $product,
'isFavorited' => $isFavorited,
]);
}
```
2. **라우트 설정** (routes/web.php)
```php
use App\Http\Controllers\HomeController;
Route::get('/', [HomeController::class, 'index'])->name('home');
Route::get('/products/{id}', [HomeController::class, 'show'])->name('products.show');
```
3. **React 페이지 컴포넌트 생성**
- resources/js/Pages/Home.jsx 생성
- props로 products (페이지네이션 객체), categories, filters 받음
- 카테고리 필터 버튼 (클릭 시 Inertia.get(route('home'), { category: 'illustration' }))
- 검색 input (onSubmit 시 Inertia.get(route('home'), { search: searchTerm }))
- 정렬 select (onChange 시 Inertia.get(route('home'), { sort: selectedSort }))
- 상품 그리드 (카드 형태, 썸네일, 제목, 크리에이터명, 가격, 평균 별점 표시)
- 각 상품 카드 클릭 시 Link to products.show
- 페이지네이션 (products.links 활용, Inertia.js 페이지네이션 컴포넌트)
- Tailwind CSS로 그리드 레이아웃, 카드 디자인
- resources/js/Pages/ProductDetail.jsx 생성
- props로 product, isFavorited 받음
- 상품 이미지 (썸네일 또는 placeholder)
- 제목, 크리에이터명, 가격, 카테고리, 상세 설명 표시
- '장바구니에 담기' 버튼 (일단 alert로 대체, 다음 단계에서 구현)
- '찜하기' 버튼 (하트 아이콘, isFavorited에 따라 색상 변경, 클릭 시 Inertia.post(route('favorites.toggle')))
- 리뷰 목록 (작성자, 별점, 댓글, 작성일)
- 리뷰 평균 별점 계산 표시
- Tailwind CSS로 레이아웃 (2컬럼: 왼쪽 이미지, 오른쪽 정보)
4. **Welcome.jsx 수정** (기본 페이지 대신 Home 페이지로 리다이렉트)
- routes/web.php의 기본 '/' 라우트를 HomeController@index로 변경했으므로, Welcome.jsx는 더 이상 사용 안 함 (삭제 또는 무시)
위 단계를 완료하고, 메인 페이지에서 상품 목록이 보이는지, 필터/검색/정렬이 작동하는지, 상품 클릭 시 상세 페이지로 이동하는지 테스트하세요.
기대 결과: HomeController에 index, show 메서드 구현 완료. 라우트 설정 완료. Home.jsx와 ProductDetail.jsx 페이지 컴포넌트 생성. 메인 페이지에서 상품 목록 그리드 표시, 카테고리 필터/검색/정렬 작동. 상품 상세 페이지에서 정보와 리뷰 목록 확인 가능.
장바구니 기능을 구현합니다. 세션 기반 장바구니를 사용하여 여러 상품을 담고, 총액을 계산하는 기능을 만드세요.
1. **CartController 생성**
- `php artisan make:controller CartController`
- app/Http/Controllers/CartController.php에 다음 메서드 추가:
- `index()`: 장바구니 페이지
```php
public function index()
{
$cart = session()->get('cart', []);
$cartItems = [];
$total = 0;
foreach ($cart as $productId => $quantity) {
$product = Product::find($productId);
if ($product) {
$cartItems[] = [
'product' => $product,
'quantity' => $quantity,
'subtotal' => $product->price * $quantity,
];
$total += $product->price * $quantity;
}
}
return Inertia::render('Cart', [
'cartItems' => $cartItems,
'total' => $total,
]);
}
```
- `add(Request $request)`: 장바구니에 상품 추가
```php
public function add(Request $request)
{
$productId = $request->product_id;
$quantity = $request->quantity ?? 1;
$cart = session()->get('cart', []);
$cart[$productId] = ($cart[$productId] ?? 0) + $quantity;
session()->put('cart', $cart);
return redirect()->back()->with('success', '장바구니에 추가되었습니다!');
}
```
- `update(Request $request)`: 수량 변경
```php
public function update(Request $request)
{
$productId = $request->product_id;
$quantity = $request->quantity;
$cart = session()->get('cart', []);
if ($quantity > 0) {
$cart[$productId] = $quantity;
} else {
unset($cart[$productId]);
}
session()->put('cart', $cart);
return redirect()->back();
}
```
- `remove(Request $request)`: 상품 제거
```php
public function remove(Request $request)
{
$productId = $request->product_id;
$cart = session()->get('cart', []);
unset($cart[$productId]);
session()->put('cart', $cart);
return redirect()->back();
}
```
2. **라우트 설정** (routes/web.php)
```php
use App\Http\Controllers\CartController;
Route::middleware('auth')->group(function () {
Route::get('/cart', [CartController::class, 'index'])->name('cart.index');
Route::post('/cart/add', [CartController::class, 'add'])->name('cart.add');
Route::post('/cart/update', [CartController::class, 'update'])->name('cart.update');
Route::post('/cart/remove', [CartController::class, 'remove'])->name('cart.remove');
});
```
3. **React 페이지 컴포넌트 생성**
- resources/js/Pages/Cart.jsx 생성
- props로 cartItems, total 받음
- 장바구니 아이템 목록 (상품 썸네일, 제목, 가격, 수량, 소계)
- 수량 조정 버튼 (+/-, Inertia.post(route('cart.update'), { product_id, quantity }))
- 삭제 버튼 (Inertia.post(route('cart.remove'), { product_id }))
- 총액 표시
- '결제하기' 버튼 (다음 단계에서 구현, 일단 Link to checkout 페이지)
- Tailwind CSS로 테이블 또는 카드 형태 레이아웃
4. **ProductDetail.jsx 수정**
- '장바구니에 담기' 버튼을 실제 작동하도록 수정
- 버튼 클릭 시: `Inertia.post(route('cart.add'), { product_id: product.id, quantity: 1 })`
- 수량 input 추가 (사용자가 원하는 수량 입력 가능)
5. **네비게이션 메뉴에 장바구니 링크 추가**
- resources/js/Layouts/AuthenticatedLayout.jsx 수정
- 상단 네비게이션에 '장바구니' 링크 추가 (Link to cart.index)
- (보너스) 장바구니 아이템 개수 뱃지 표시 (session cart 데이터를 Inertia shared data로 전달)
위 단계를 완료하고, 상품 상세 페이지에서 장바구니에 담기, 장바구니 페이지에서 수량 조정/삭제, 총액 계산이 정상 작동하는지 테스트하세요.
기대 결과: CartController에 index, add, update, remove 메서드 구현 완료. 라우트 설정 완료. Cart.jsx 페이지 컴포넌트 생성. 세션 기반 장바구니 작동 확인. 상품 상세 페이지에서 장바구니에 담기, 장바구니 페이지에서 수량 조정/삭제/총액 표시 정상 작동.
주문/결제 시뮬레이션과 구매 내역, 파일 다운로드 기능을 구현합니다. 실제 결제 API 연동 없이 '결제 완료' 버튼으로 주문을 생성하고, 구매한 상품을 다운로드할 수 있게 만드세요.
1. **OrderController 생성**
- `php artisan make:controller OrderController`
- app/Http/Controllers/OrderController.php에 다음 메서드 추가:
- `checkout()`: 결제 페이지 (장바구니 내용 확인)
```php
public function checkout()
{
$cart = session()->get('cart', []);
$cartItems = [];
$total = 0;
foreach ($cart as $productId => $quantity) {
$product = Product::find($productId);
if ($product) {
$cartItems[] = [
'product' => $product,
'quantity' => $quantity,
'subtotal' => $product->price * $quantity,
];
$total += $product->price * $quantity;
}
}
return Inertia::render('Checkout', [
'cartItems' => $cartItems,
'total' => $total,
]);
}
```
- `store()`: 주문 생성 (결제 완료 시뮬레이션)
```php
public function store()
{
$cart = session()->get('cart', []);
if (empty($cart)) {
return redirect()->route('cart.index')->with('error', '장바구니가 비어있습니다.');
}
$total = 0;
$order = auth()->user()->orders()->create(['total_price' => 0, 'status' => 'completed']);
foreach ($cart as $productId => $quantity) {
$product = Product::find($productId);
if ($product) {
$subtotal = $product->price * $quantity;
$order->items()->create([
'product_id' => $productId,
'quantity' => $quantity,
'price' => $subtotal,
]);
$total += $subtotal;
// 다운로드 수 증가
$product->increment('downloads_count', $quantity);
}
}
$order->update(['total_price' => $total]);
session()->forget('cart'); // 장바구니 비우기
return redirect()->route('orders.index')->with('success', '결제가 완료되었습니다!');
}
```
- `index()`: 구매 내역 페이지
```php
public function index()
{
$orders = auth()->user()->orders()->with('items.product')->latest()->get();
return Inertia::render('MyOrders', ['orders' => $orders]);
}
```
- `download($productId)`: 디지털 파일 다운로드
```php
public function download($productId)
{
$order = auth()->user()->orders()->whereHas('items', function ($query) use ($productId) {
$query->where('product_id', $productId);
})->first();
if (!$order) {
abort(403, '구매하지 않은 상품입니다.');
}
$product = Product::findOrFail($productId);
$filePath = storage_path('app/public/' . $product->file_path);
if (!file_exists($filePath)) {
abort(404, '파일을 찾을 수 없습니다.');
}
return response()->download($filePath, $product->title . '.' . pathinfo($filePath, PATHINFO_EXTENSION));
}
```
2. **라우트 설정** (routes/web.php)
```php
use App\Http\Controllers\OrderController;
Route::middleware('auth')->group(function () {
Route::get('/checkout', [OrderController::class, 'checkout'])->name('checkout');
Route::post('/orders', [OrderController::class, 'store'])->name('orders.store');
Route::get('/orders', [OrderController::class, 'index'])->name('orders.index');
Route::get('/orders/download/{productId}', [OrderController::class, 'download'])->name('orders.download');
});
```
3. **React 페이지 컴포넌트 생성**
- resources/js/Pages/Checkout.jsx 생성
- props로 cartItems, total 받음
- 주문 내역 확인 (장바구니와 동일하게 상품 목록, 총액 표시)
- '결제 완료' 버튼 (Inertia.post(route('orders.store')))
- Tailwind CSS로 주문 요약 카드 디자인
- resources/js/Pages/MyOrders.jsx 생성
- props로 orders 받음
- 주문 목록 (주문 번호, 주문일, 총액, 상태)
- 각 주문의 상품 목록 (제목, 수량, 가격)
- 각 상품마다 '다운로드' 버튼 (a href={route('orders.download', product.id)})
- Tailwind CSS로 주문 카드 레이아웃
4. **Cart.jsx 수정**
- '결제하기' 버튼을 Link to checkout으로 변경
5. **네비게이션 메뉴에 구매 내역 링크 추가**
- resources/js/Layouts/AuthenticatedLayout.jsx 수정
- 상단 네비게이션에 '구매 내역' 링크 추가 (Link to orders.index)
위 단계를 완료하고, 장바구니에서 결제하기 → 주문 생성 → 구매 내역에서 다운로드 버튼 클릭 시 파일 다운로드가 정상 작동하는지 테스트하세요. 다운로드 수가 증가하는지도 확인하세요.
기대 결과: OrderController에 checkout, store, index, download 메서드 구현 완료. 라우트 설정 완료. Checkout.jsx와 MyOrders.jsx 페이지 컴포넌트 생성. 결제 시뮬레이션으로 주문 생성, 구매 내역에서 파일 다운로드 정상 작동. 다운로드 수 증가 확인.
마지막으로 리뷰 작성 기능과 찜하기(좋아요) 기능을 구현합니다. 사용자가 구매한 상품에 한해 리뷰를 작성할 수 있고, 마음에 드는 상품을 찜할 수 있게 만드세요.
1. **ReviewController 생성**
- `php artisan make:controller ReviewController`
- app/Http/Controllers/ReviewController.php에 다음 메서드 추가:
- `store(Request $request)`: 리뷰 작성
```php
public function store(Request $request)
{
$validated = $request->validate([
'product_id' => 'required|exists:products,id',
'rating' => 'required|integer|min:1|max:5',
'comment' => 'nullable|string|max:1000',
]);
// 구매한 상품인지 확인
$hasPurchased = auth()->user()->orders()->whereHas('items', function ($query) use ($validated) {
$query->where('product_id', $validated['product_id']);
})->exists();
if (!$hasPurchased) {
return redirect()->back()->with('error', '구매한 상품만 리뷰를 작성할 수 있습니다.');
}
// 이미 리뷰 작성했는지 확인
$existingReview = auth()->user()->reviews()->where('product_id', $validated['product_id'])->first();
if ($existingReview) {
return redirect()->back()->with('error', '이미 리뷰를 작성하셨습니다.');
}
auth()->user()->reviews()->create($validated);
// 상품 평균 별점 업데이트
$product = Product::find($validated['product_id']);
$avgRating = $product->reviews()->avg('rating');
$product->update(['rating_avg' => $avgRating]);
return redirect()->back()->with('success', '리뷰가 작성되었습니다!');
}
```
- User 모델에 reviews 관계 추가 (app/Models/User.php)
```php
public function reviews()
{
return $this->hasMany(Review::class);
}
```
- Review 모델에 fillable 추가 (app/Models/Review.php)
```php
protected $fillable = ['user_id', 'product_id', 'rating', 'comment'];
```
2. **FavoriteController 생성**
- `php artisan make:controller FavoriteController`
- app/Http/Controllers/FavoriteController.php에 다음 메서드 추가:
- `toggle(Request $request)`: 찜하기/찜 해제 토글
```php
public function toggle(Request $request)
{
$productId = $request->product_id;
$favorite = auth()->user()->favorites()->where('product_id', $productId)->first();
if ($favorite) {
$favorite->delete();
$message = '찜 목록에서 제거되었습니다.';
} else {
auth()->user()->favorites()->attach($productId);
$message = '찜 목록에 추가되었습니다.';
}
return redirect()->back()->with('success', $message);
}
```
- `index()`: 찜한 상품 목록 페이지
```php
public function index()
{
$favorites = auth()->user()->favorites()->with('user')->latest('favorites.created_at')->get();
return Inertia::render('Favorites', ['favorites' => $favorites]);
}
```
3. **라우트 설정** (routes/web.php)
```php
use App\Http\Controllers\ReviewController;
use App\Http\Controllers\FavoriteController;
Route::middleware('auth')->group(function () {
Route::post('/reviews', [ReviewController::class, 'store'])->name('reviews.store');
Route::post('/favorites/toggle', [FavoriteController::class, 'toggle'])->name('favorites.toggle');
Route::get('/favorites', [FavoriteController::class, 'index'])->name('favorites.index');
});
```
4. **React 페이지 컴포넌트 수정 및 생성**
- resources/js/Pages/ProductDetail.jsx 수정
- 리뷰 작성 폼 추가 (구매한 사용자만 표시)
- 별점 선택 (1~5, 별 아이콘 클릭)
- 댓글 textarea
- 제출 버튼 (Inertia.post(route('reviews.store'), { product_id, rating, comment }))
- 찜하기 버튼 수정
- 클릭 시 Inertia.post(route('favorites.toggle'), { product_id: product.id })
- isFavorited에 따라 하트 아이콘 색상 변경 (빨강/회색)
- resources/js/Pages/Favorites.jsx 생성
- props로 favorites 받음
- 찜한 상품 그리드 (Home.jsx와 유사한 카드 레이아웃)
- 각 상품 카드에 '찜 해제' 버튼 (Inertia.post(route('favorites.toggle'), { product_id }))
- Tailwind CSS로 그리드 디자인
5. **네비게이션 메뉴에 찜 목록 링크 추가**
- resources/js/Layouts/AuthenticatedLayout.jsx 수정
- 상단 네비게이션에 '찜 목록' 링크 추가 (Link to favorites.index)
6. **테스트 데이터 추가** (선택 사항)
- Tinker 또는 시더로 테스트용 상품 5~10개 생성
- `php artisan tinker`
- `User::factory()->create(['email' => 'creator@test.com']);`
- `Product::factory()->count(10)->create(['user_id' => 1]);`
- (Product 팩토리가 없다면 수동으로 생성)
위 단계를 완료하고, 상품 상세 페이지에서 리뷰 작성, 찜하기 버튼 작동 확인. 찜 목록 페이지에서 찜한 상품 목록 표시 확인. 구매하지 않은 상품에 리뷰 작성 시도 시 에러 메시지 표시 확인.
모든 기능이 정상 작동하면 프로젝트 완성입니다! 🎉
기대 결과: ReviewController와 FavoriteController 구현 완료. 라우트 설정 완료. ProductDetail.jsx에 리뷰 작성 폼 추가, 찜하기 버튼 작동. Favorites.jsx 페이지 생성. 구매한 상품만 리뷰 작성 가능, 찜하기/찜 해제 토글 정상 작동. 평균 별점 업데이트 확인. 모든 기능 통합 테스트 완료.
제출된 작품 (0)
로그인 후 제출아직 제출된 작품이 없습니다. 첫 번째 도전자가 되어보세요!