크리에이터 디지털 굿즈 스토어 - 작품 판매 & 구매자 리뷰 - 바이브코딩 레시피
중급 PHP + JavaScript Laravel + React + Inertia.js AI: Claude

크리에이터 디지털 굿즈 스토어 - 작품 판매 & 구매자 리뷰

리포지토리명 creator-digital-goods-store
조회수 58 0건 제출

문제 설명

🎨 크리에이터를 위한 디지털 굿즈 판매 플랫폼

일러스트레이터, 디자이너, 작가 등 크리에이터가 자신의 디지털 작품(이미지, 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에게 보내면 됩니다. 복사 버튼을 눌러 바로 사용하세요.

1
Step 1
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 컴포넌트 존재.

2
Step 2
이제 데이터베이스 구조를 설계합니다. 크리에이터 디지털 굿즈 스토어에 필요한 테이블과 모델을 생성하세요.

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 실행 시 에러 없이 테이블 생성 확인.

3
Step 3
이제 크리에이터 대시보드와 상품 등록 기능을 구현합니다. 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에 저장.

4
Step 4
이제 일반 사용자가 볼 수 있는 상품 목록 페이지와 상품 상세 페이지를 구현합니다. 필터링, 검색, 정렬 기능도 추가하세요.

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 페이지 컴포넌트 생성. 메인 페이지에서 상품 목록 그리드 표시, 카테고리 필터/검색/정렬 작동. 상품 상세 페이지에서 정보와 리뷰 목록 확인 가능.

5
Step 5
장바구니 기능을 구현합니다. 세션 기반 장바구니를 사용하여 여러 상품을 담고, 총액을 계산하는 기능을 만드세요.

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 페이지 컴포넌트 생성. 세션 기반 장바구니 작동 확인. 상품 상세 페이지에서 장바구니에 담기, 장바구니 페이지에서 수량 조정/삭제/총액 표시 정상 작동.

6
Step 6
주문/결제 시뮬레이션과 구매 내역, 파일 다운로드 기능을 구현합니다. 실제 결제 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 페이지 컴포넌트 생성. 결제 시뮬레이션으로 주문 생성, 구매 내역에서 파일 다운로드 정상 작동. 다운로드 수 증가 확인.

7
Step 7
마지막으로 리뷰 작성 기능과 찜하기(좋아요) 기능을 구현합니다. 사용자가 구매한 상품에 한해 리뷰를 작성할 수 있고, 마음에 드는 상품을 찜할 수 있게 만드세요.

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)

로그인 후 제출

아직 제출된 작품이 없습니다. 첫 번째 도전자가 되어보세요!

댓글 (0)

로그인 후 댓글을 남길 수 있습니다.
아직 댓글이 없습니다. 첫 번째 댓글을 남겨보세요!