동네 중고마켓 - 실시간 채팅 & 찜하기 - 바이브코딩 레시피
중급 PHP + JavaScript Laravel + React + Inertia.js AI: Claude

동네 중고마켓 - 실시간 채팅 & 찜하기

리포지토리명 local-secondhand-market-chat
조회수 78 0건 제출

문제 설명

🛒 동네 중고마켓 플랫폼

당근마켓, 번개장터처럼 사용자가 중고 물품을 등록하고 거래할 수 있는 풀스택 플랫폼을 만듭니다. Laravel 13.x + Inertia.js + React 19 모노리스 구조로, 상품 CRUD부터 실시간 채팅(Laravel Reverb), 찜하기, 검색/필터링까지 실제 서비스 수준의 기능을 구현합니다.

필수 기능

  • 회원가입/로그인 (Laravel Breeze 기본 인증)
  • 상품 등록/수정/삭제 (이미지 업로드 포함)
  • 상품 목록 (카드 그리드 레이아웃, 무한 스크롤)
  • 상품 상세 페이지 (판매자 정보, 상태 표시)
  • 찜하기/찜 목록 (하트 아이콘 토글)
  • 실시간 1:1 채팅 (Laravel Reverb + WebSocket)
  • 카테고리별 필터링 (전자기기, 의류, 가구, 도서 등)
  • 검색 기능 (제목/내용 기반)
  • 반응형 디자인 (모바일 퍼스트)

보너스 기능

  • 판매 완료 상태 전환
  • 내 판매 물품 관리 페이지
  • 읽지 않은 채팅 알림 뱃지
  • 가격 제안 기능
  • 거래 후기 시스템

기술 스택

  • Backend: Laravel 13.x (Eloquent, SQLite)
  • Frontend: React 19 + Inertia.js 2.x
  • Real-time: Laravel Reverb (WebSocket)
  • Styling: Tailwind CSS 4.x
  • File Upload: Laravel Storage (public disk)

실제 사용자가 물건을 사고파는 경험을 구현하며, WebSocket 기반 실시간 채팅으로 인터랙션을 극대화합니다.

제약 조건

  • • Laravel 13.x + Inertia.js 2.x + React 19 모노리스 구조 필수
  • • Laravel Breeze React+Inertia starter kit으로 프로젝트 생성
  • • 별도 REST API 라우트 없이 Inertia::render()로 데이터 전달
  • • Laravel Reverb로 실시간 채팅 구현 (config/broadcasting.php 설정)
  • • SQLite 데이터베이스 사용 (마이그레이션 파일로 스키마 정의)
  • • 이미지 업로드는 Laravel Storage public disk 사용
  • • Tailwind CSS 4.x로 스타일링 (모바일 퍼스트 반응형)
  • • React 컴포넌트는 resources/js/Pages, resources/js/Components 디렉토리에 구성
  • • 찜하기는 다대다 관계 (pivot table) 활용
  • • 채팅 메시지는 Eloquent 모델로 저장 후 broadcast
  • • 검색/필터는 Laravel Eloquent where/orWhere 쿼리 활용
  • • 인증은 Laravel Sanctum (Breeze 기본 제공)

프롬프트 레시피

아래 프롬프트를 순서대로 AI에게 보내면 됩니다. 복사 버튼을 눌러 바로 사용하세요.

1
Step 1
mkdir local-secondhand-market-chat && cd local-secondhand-market-chat

이 폴더에서 Laravel 13.x + Inertia.js + React 19 프로젝트를 시작합니다.

**요구사항:**
1. 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
   ```
2. 데이터베이스를 SQLite로 설정 (.env 파일에서 DB_CONNECTION=sqlite, database/database.sqlite 파일 생성)
3. 마이그레이션 실행 (php artisan migrate)
4. 개발 서버 실행 확인 (php artisan serve, npm run dev)
5. 브라우저에서 http://localhost:8000 접속 시 Breeze 기본 페이지(로그인/회원가입) 표시 확인

**추가 설정:**
- Tailwind CSS 4.x가 Breeze에 포함되어 있으므로 별도 설치 불필요
- Inertia.js 2.x도 Breeze에 포함됨
- resources/js/Pages 디렉토리에 React 페이지 컴포넌트가 있는지 확인

모든 설정이 완료되면 "Laravel Breeze + Inertia + React 프로젝트 생성 완료" 메시지를 출력하세요.

기대 결과: Laravel 13.x + Breeze + Inertia.js + React 19 프로젝트가 생성되고, SQLite 데이터베이스 연결 및 기본 인증 페이지가 작동하는 상태. http://localhost:8000에서 로그인/회원가입 페이지 확인 가능.

2
Step 2
**상품(Product) 모델 및 마이그레이션 생성**

중고마켓의 핵심인 상품 데이터를 저장할 구조를 만듭니다.

1. Product 모델과 마이그레이션 생성:
   ```bash
   php artisan make:model Product -m
   ```

2. 마이그레이션 파일 (database/migrations/xxxx_create_products_table.php) 스키마:
   - id (primary key)
   - user_id (외래키, users 테이블 참조 - 판매자)
   - title (string, 상품 제목)
   - description (text, 상품 설명)
   - price (integer, 가격)
   - category (string, 카테고리: 전자기기/의류/가구/도서/기타)
   - status (string, 판매중/예약중/판매완료, 기본값: 판매중)
   - image_path (string, nullable, 이미지 경로)
   - timestamps (created_at, updated_at)
   - 외래키 제약: user_id는 users.id 참조, onDelete('cascade')

3. Product 모델 (app/Models/Product.php):
   - fillable 속성: ['user_id', 'title', 'description', 'price', 'category', 'status', 'image_path']
   - belongsTo 관계: user() - User 모델과 연결
   - belongsToMany 관계: favoritedByUsers() - User 모델과 다대다 (찜하기용, 나중에 pivot 테이블 생성)

4. User 모델 (app/Models/User.php)에 관계 추가:
   - hasMany 관계: products() - Product 모델
   - belongsToMany 관계: favoriteProducts() - Product 모델 (찜한 상품)

5. 마이그레이션 실행:
   ```bash
   php artisan migrate
   ```

완료 후 "Product 모델 및 마이그레이션 생성 완료" 메시지 출력.

기대 결과: products 테이블이 데이터베이스에 생성되고, Product 모델이 User와 관계 설정된 상태. php artisan tinker로 Product::count() 실행 시 0 반환 확인.

3
Step 3
**상품 등록 기능 구현 (이미지 업로드 포함)**

사용자가 중고 물품을 등록할 수 있는 폼과 컨트롤러를 만듭니다.

1. ProductController 생성:
   ```bash
   php artisan make:controller ProductController
   ```

2. 라우트 등록 (routes/web.php):
   ```php
   use App\Http\Controllers\ProductController;

   Route::middleware('auth')->group(function () {
       Route::get('/products/create', [ProductController::class, 'create'])->name('products.create');
       Route::post('/products', [ProductController::class, 'store'])->name('products.store');
   });
   ```

3. ProductController 메서드:
   - create(): Inertia::render('Products/Create') 반환 (카테고리 목록 전달)
   - store(Request $request):
     - 검증: title(필수, 최대 100자), description(필수), price(필수, 정수, 최소 0), category(필수, in:전자기기,의류,가구,도서,기타), image(nullable, 이미지 파일, 최대 2MB)
     - 이미지 업로드 처리: $request->file('image')->store('products', 'public') (Storage facade 사용)
     - Product 생성: auth()->user()->products()->create([...]) (user_id 자동 설정)
     - 리다이렉트: redirect()->route('products.index')->with('success', '상품이 등록되었습니다.')

4. React 페이지 컴포넌트 (resources/js/Pages/Products/Create.jsx):
   - Inertia useForm 훅 사용:
     ```jsx
     const { data, setData, post, processing, errors } = useForm({
         title: '',
         description: '',
         price: '',
         category: '전자기기',
         image: null,
     });
     ```
   - 폼 필드: title(input), description(textarea), price(input number), category(select - 전자기기/의류/가구/도서/기타), image(file input)
   - onSubmit: post(route('products.store')) (multipart/form-data 자동 처리)
   - Tailwind로 스타일링 (카드 레이아웃, 폼 요소 간격, 버튼 디자인)

5. storage 링크 생성 (이미지 접근용):
   ```bash
   php artisan storage:link
   ```

6. 네비게이션에 "상품 등록" 링크 추가 (resources/js/Layouts/AuthenticatedLayout.jsx):
   ```jsx
   <Link href={route('products.create')}>상품 등록</Link>
   ```

완료 후 로그인한 사용자가 상품 등록 폼에 접근하여 제출 시 데이터베이스에 저장되는지 확인하세요.

기대 결과: 로그인한 사용자가 /products/create 페이지에서 상품 정보와 이미지를 입력하여 등록할 수 있음. 등록 후 products 테이블에 데이터 저장 확인.

4
Step 4
**상품 목록 페이지 (카드 그리드 + 무한 스크롤)**

등록된 상품을 카드 형태로 보여주는 메인 페이지를 만듭니다.

1. 라우트 추가 (routes/web.php):
   ```php
   Route::get('/', [ProductController::class, 'index'])->name('products.index');
   Route::get('/products', [ProductController::class, 'index']); // 동일 페이지
   ```

2. ProductController::index() 메서드:
   - 상품 목록 조회: Product::with('user')->latest()->paginate(12) (판매자 정보 eager loading)
   - Inertia::render('Products/Index', ['products' => $products]) 반환

3. React 페이지 컴포넌트 (resources/js/Pages/Products/Index.jsx):
   - props로 products 받기 (Inertia pagination 객체)
   - 그리드 레이아웃: grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-6
   - 각 상품 카드:
     - 이미지 (있으면 표시, 없으면 placeholder)
     - 제목, 가격 (천 단위 콤마 포맷팅)
     - 판매자 이름
     - 상태 뱃지 (판매중: 초록, 예약중: 노랑, 판매완료: 회색)
     - 카드 클릭 시 상세 페이지로 이동 (Link href={route('products.show', product.id)})
   - 무한 스크롤 구현:
     - IntersectionObserver 또는 react-infinite-scroll-component 라이브러리 사용
     - products.next_page_url이 있으면 다음 페이지 로드
     - router.reload() 또는 Inertia.get()으로 추가 데이터 가져오기
   - Tailwind로 스타일링 (카드 그림자, 호버 효과, 반응형)

4. 이미지 표시:
   - 이미지 경로: /storage/{image_path} (storage 링크 설정됨)
   - 없으면 placeholder (예: https://via.placeholder.com/300x200?text=No+Image)

완료 후 메인 페이지에서 등록된 상품들이 카드 형태로 표시되고, 스크롤 시 추가 상품이 로드되는지 확인하세요.

기대 결과: / 경로에서 상품 목록이 카드 그리드로 표시되고, 무한 스크롤로 추가 상품이 로드됨. 각 카드에 이미지, 제목, 가격, 판매자, 상태가 표시됨.

5
Step 5
**상품 상세 페이지 & 찜하기 기능**

상품 클릭 시 상세 정보를 보고 찜하기를 할 수 있는 페이지를 만듭니다.

1. 찜하기용 pivot 테이블 마이그레이션 생성:
   ```bash
   php artisan make:migration create_favorites_table
   ```
   - 스키마: user_id, product_id (복합 유니크 인덱스), timestamps
   - 외래키: user_id → users.id, product_id → products.id (onDelete cascade)
   - 마이그레이션 실행: php artisan migrate

2. User 모델에 favoriteProducts() 관계 추가:
   ```php
   public function favoriteProducts()
   {
       return $this->belongsToMany(Product::class, 'favorites')->withTimestamps();
   }
   ```

3. Product 모델에 favoritedByUsers() 관계 추가:
   ```php
   public function favoritedByUsers()
   {
       return $this->belongsToMany(User::class, 'favorites')->withTimestamps();
   }
   ```

4. 라우트 추가 (routes/web.php):
   ```php
   Route::get('/products/{product}', [ProductController::class, 'show'])->name('products.show');
   Route::middleware('auth')->group(function () {
       Route::post('/products/{product}/favorite', [ProductController::class, 'toggleFavorite'])->name('products.favorite');
   });
   ```

5. ProductController::show(Product $product) 메서드:
   - 상품 로드: $product->load('user', 'favoritedByUsers')
   - 현재 사용자가 찜했는지 확인: $isFavorited = auth()->check() && auth()->user()->favoriteProducts->contains($product->id)
   - Inertia::render('Products/Show', ['product' => $product, 'isFavorited' => $isFavorited])

6. ProductController::toggleFavorite(Product $product) 메서드:
   - auth()->user()->favoriteProducts()->toggle($product->id) (찜하기/취소 토글)
   - return back()

7. React 페이지 컴포넌트 (resources/js/Pages/Products/Show.jsx):
   - props로 product, isFavorited 받기
   - 레이아웃: 이미지(왼쪽), 정보(오른쪽) - 모바일에서는 세로 배치
   - 표시 정보: 제목, 가격, 카테고리, 상태, 설명, 판매자 이름, 등록일
   - 찜하기 버튼:
     - 하트 아이콘 (isFavorited면 빨간색 채움, 아니면 테두리만)
     - 클릭 시 router.post(route('products.favorite', product.id))
     - Inertia useForm 또는 router 사용
   - 판매자가 본인이면 "수정/삭제" 버튼 표시 (추후 구현)
   - 판매자가 타인이면 "채팅하기" 버튼 표시 (다음 단계에서 구현)
   - Tailwind로 스타일링 (카드, 버튼, 반응형)

완료 후 상품 상세 페이지에서 찜하기 버튼 클릭 시 토글되고, favorites 테이블에 레코드가 생성/삭제되는지 확인하세요.

기대 결과: 상품 상세 페이지(/products/{id})에서 이미지, 정보, 찜하기 버튼이 표시되고, 찜하기 클릭 시 하트 색상이 토글되며 데이터베이스에 반영됨.

6
Step 6
**실시간 1:1 채팅 기능 (Laravel Reverb + WebSocket)**

상품 상세 페이지에서 판매자와 1:1 채팅을 할 수 있는 기능을 구현합니다.

1. Laravel Reverb 설치 및 설정:
   ```bash
   php artisan install:broadcasting
   ```
   - .env 파일에서 BROADCAST_DRIVER=reverb 설정 확인
   - config/broadcasting.php에서 reverb 설정 확인
   - Reverb 서버 실행: php artisan reverb:start (별도 터미널)

2. Message 모델 및 마이그레이션 생성:
   ```bash
   php artisan make:model Message -m
   ```
   - 스키마: id, sender_id(외래키, users), receiver_id(외래키, users), product_id(외래키, products), content(text), read_at(timestamp, nullable), timestamps
   - 외래키 제약: sender_id/receiver_id → users.id, product_id → products.id (onDelete cascade)
   - 마이그레이션 실행

3. Message 모델 (app/Models/Message.php):
   - fillable: ['sender_id', 'receiver_id', 'product_id', 'content', 'read_at']
   - belongsTo 관계: sender(), receiver(), product()

4. MessageSent 이벤트 생성:
   ```bash
   php artisan make:event MessageSent
   ```
   - implements ShouldBroadcast
   - public $message (Message 모델 인스턴스)
   - broadcastOn(): PrivateChannel('chat.{receiver_id}')
   - broadcastWith(): ['message' => $message->load('sender')]

5. 라우트 추가 (routes/web.php):
   ```php
   Route::middleware('auth')->group(function () {
       Route::get('/chat/{product}/{receiver}', [ChatController::class, 'show'])->name('chat.show');
       Route::post('/chat/{product}/{receiver}', [ChatController::class, 'send'])->name('chat.send');
   });
   ```

6. ChatController 생성 및 메서드:
   ```bash
   php artisan make:controller ChatController
   ```
   - show(Product $product, User $receiver): 해당 상품 관련 두 사용자 간 메시지 조회, Inertia::render('Chat/Show', ['product' => $product, 'receiver' => $receiver, 'messages' => $messages])
   - send(Request $request, Product $product, User $receiver): 메시지 생성 후 MessageSent 이벤트 broadcast, return back()

7. React 페이지 컴포넌트 (resources/js/Pages/Chat/Show.jsx):
   - props로 product, receiver, messages 받기
   - 채팅 UI: 상단에 상품 정보 미리보기, 메시지 목록(스크롤 가능), 하단에 입력 폼
   - 메시지 목록: 내가 보낸 메시지는 오른쪽 정렬(파란색), 받은 메시지는 왼쪽 정렬(회색)
   - 입력 폼: useForm으로 content 전송, post(route('chat.send', [product.id, receiver.id]))
   - Laravel Echo로 실시간 수신:
     ```jsx
     import Echo from 'laravel-echo';
     import Pusher from 'pusher-js';
     window.Pusher = Pusher;
     window.Echo = new Echo({
         broadcaster: 'reverb',
         key: import.meta.env.VITE_REVERB_APP_KEY,
         wsHost: import.meta.env.VITE_REVERB_HOST,
         wsPort: import.meta.env.VITE_REVERB_PORT,
         forceTLS: false,
     });

     useEffect(() => {
         Echo.private(`chat.${auth.user.id}`)
             .listen('MessageSent', (e) => {
                 setMessages([...messages, e.message]);
             });
         return () => Echo.leave(`chat.${auth.user.id}`);
     }, [messages]);
     ```
   - Tailwind로 채팅 UI 스타일링 (말풍선, 스크롤, 반응형)

8. 상품 상세 페이지(Products/Show.jsx)에 "채팅하기" 버튼 추가:
   - 판매자가 아닌 경우에만 표시
   - 클릭 시 Link href={route('chat.show', [product.id, product.user.id])}

9. .env 파일에 Reverb 설정 추가:
   ```
   REVERB_APP_ID=your-app-id
   REVERB_APP_KEY=your-app-key
   REVERB_APP_SECRET=your-app-secret
   REVERB_HOST=127.0.0.1
   REVERB_PORT=8080
   REVERB_SCHEME=http
   ```

완료 후 상품 상세 페이지에서 "채팅하기" 버튼 클릭 시 채팅 페이지로 이동하고, 메시지 전송 시 실시간으로 상대방 화면에 표시되는지 확인하세요 (Reverb 서버 실행 필수).

기대 결과: 상품 상세 페이지에서 "채팅하기" 버튼 클릭 시 1:1 채팅 페이지로 이동하고, 메시지 전송 시 WebSocket을 통해 실시간으로 상대방 화면에 표시됨. Reverb 서버가 실행 중이어야 함.

7
Step 7
**검색 & 카테고리 필터링 기능**

상품 목록 페이지에 검색창과 카테고리 필터를 추가하여 사용자가 원하는 상품을 쉽게 찾을 수 있도록 합니다.

1. ProductController::index() 메서드 수정:
   - 검색어(search), 카테고리(category), 상태(status) 쿼리 파라미터 받기
   - Eloquent 쿼리:
     ```php
     $query = Product::with('user')->latest();
     if ($request->search) {
         $query->where(function($q) use ($request) {
             $q->where('title', 'like', '%'.$request->search.'%')
               ->orWhere('description', 'like', '%'.$request->search.'%');
         });
     }
     if ($request->category) {
         $query->where('category', $request->category);
     }
     if ($request->status) {
         $query->where('status', $request->status);
     }
     $products = $query->paginate(12)->withQueryString(); // 쿼리 파라미터 유지
     ```
   - Inertia::render()에 filters도 전달: ['products' => $products, 'filters' => $request->only(['search', 'category', 'status'])]

2. React 페이지 컴포넌트 (resources/js/Pages/Products/Index.jsx) 수정:
   - props로 filters 받기
   - 검색창 추가:
     - input type="text" value={filters.search || ''}
     - onChange 시 router.get(route('products.index'), { search: value }, { preserveState: true })
     - debounce 적용 (lodash.debounce 또는 useEffect로 500ms 지연)
   - 카테고리 필터 버튼:
     - 전체/전자기기/의류/가구/도서/기타 버튼
     - 클릭 시 router.get(route('products.index'), { category: value })
     - 현재 선택된 카테고리는 강조 표시 (bg-blue-500 text-white)
   - 상태 필터 버튼:
     - 전체/판매중/예약중/판매완료 버튼
     - 클릭 시 router.get(route('products.index'), { status: value })
   - 필터 초기화 버튼:
     - 클릭 시 router.get(route('products.index')) (모든 필터 제거)
   - Tailwind로 스타일링 (검색창, 필터 버튼 그룹, 반응형)

3. 검색 결과 없을 때 메시지 표시:
   - products.data.length === 0이면 "검색 결과가 없습니다." 표시

완료 후 상품 목록 페이지에서 검색어 입력 시 실시간으로 필터링되고, 카테고리/상태 버튼 클릭 시 해당 상품만 표시되는지 확인하세요.

기대 결과: 상품 목록 페이지 상단에 검색창과 카테고리/상태 필터 버튼이 표시되고, 검색어 입력 또는 필터 선택 시 URL 쿼리 파라미터가 변경되며 상품 목록이 실시간으로 필터링됨.

8
Step 8
**내 찜 목록 & 내 판매 물품 페이지**

사용자가 찜한 상품과 본인이 등록한 상품을 관리할 수 있는 페이지를 만듭니다.

1. 라우트 추가 (routes/web.php):
   ```php
   Route::middleware('auth')->group(function () {
       Route::get('/favorites', [ProductController::class, 'favorites'])->name('products.favorites');
       Route::get('/my-products', [ProductController::class, 'myProducts'])->name('products.my');
       Route::patch('/products/{product}/status', [ProductController::class, 'updateStatus'])->name('products.updateStatus');
   });
   ```

2. ProductController::favorites() 메서드:
   - 현재 사용자가 찜한 상품 조회: auth()->user()->favoriteProducts()->with('user')->latest('favorites.created_at')->paginate(12)
   - Inertia::render('Products/Favorites', ['products' => $products])

3. ProductController::myProducts() 메서드:
   - 현재 사용자가 등록한 상품 조회: auth()->user()->products()->latest()->paginate(12)
   - Inertia::render('Products/MyProducts', ['products' => $products])

4. ProductController::updateStatus(Request $request, Product $product) 메서드:
   - 권한 확인: if ($product->user_id !== auth()->id()) abort(403)
   - 검증: $request->validate(['status' => 'required|in:판매중,예약중,판매완료'])
   - 상태 업데이트: $product->update(['status' => $request->status])
   - return back()->with('success', '상태가 변경되었습니다.')

5. React 페이지 컴포넌트 (resources/js/Pages/Products/Favorites.jsx):
   - Index.jsx와 유사한 카드 그리드 레이아웃
   - 각 카드에 "찜 해제" 버튼 추가 (하트 아이콘 클릭 시 router.post(route('products.favorite', product.id)))
   - 찜한 상품이 없으면 "찜한 상품이 없습니다." 메시지 표시

6. React 페이지 컴포넌트 (resources/js/Pages/Products/MyProducts.jsx):
   - Index.jsx와 유사한 카드 그리드 레이아웃
   - 각 카드에 상태 변경 드롭다운 추가:
     - select 요소로 판매중/예약중/판매완료 선택
     - onChange 시 router.patch(route('products.updateStatus', product.id), { status: value })
   - "수정" 버튼 추가 (Link href={route('products.edit', product.id)}) - 다음 단계에서 구현
   - 등록한 상품이 없으면 "등록한 상품이 없습니다." 메시지 표시

7. 네비게이션에 링크 추가 (resources/js/Layouts/AuthenticatedLayout.jsx):
   - "내 찜 목록" 링크: Link href={route('products.favorites')}
   - "내 판매 물품" 링크: Link href={route('products.my')}

완료 후 로그인한 사용자가 찜한 상품 목록과 본인이 등록한 상품 목록을 확인하고, 판매 상태를 변경할 수 있는지 확인하세요.

기대 결과: /favorites 페이지에서 찜한 상품 목록이 표시되고, /my-products 페이지에서 본인이 등록한 상품 목록과 상태 변경 드롭다운이 표시됨. 상태 변경 시 데이터베이스에 반영됨.

제출된 작품 (0)

로그인 후 제출

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

댓글 (0)

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