팀 브레인스토밍 보드 - 아이디어 투표 & 카테고리 정리
team-brainstorm-voting-board
문제 설명
🎯 프로젝트 개요
팀원들이 자유롭게 아이디어를 제안하고, 다른 팀원들이 투표하여 인기 있는 아이디어를 선별할 수 있는 협업 브레인스토밍 보드를 만듭니다. Laravel 13.x + Inertia.js + React 19 모노리스 풀스택 구조로 구현하며, 실시간성은 없지만 페이지 새로고침 없이 부드럽게 동작하는 SPA 경험을 제공합니다.
✅ 필수 기능
- 아이디어 카드 작성: 제목, 설명, 카테고리(기능개선/신규기능/UI-UX/기타) 선택하여 아이디어 등록
- 투표 시스템: 각 아이디어에 좋아요(👍) 투표 가능, 한 사용자당 한 아이디어에 1표만 (중복 투표 방지)
- 카테고리별 필터링: 특정 카테고리만 보기, 전체 보기 탭 전환
- 인기순 정렬: 투표수 기준 내림차순 정렬 기능
- 아이디어 수정/삭제: 본인이 작성한 아이디어만 수정/삭제 가능
- 반응형 카드 레이아웃: Tailwind CSS로 그리드 형태의 카드 UI
🎁 보너스 기능
- 아이디어 상세 모달: 카드 클릭 시 상세 내용 팝업
- 검색 기능: 제목/설명 키워드 검색
- 투표한 아이디어 하이라이트: 내가 투표한 카드 시각적 표시
- 최근 등록순 정렬 옵션 추가
- 아이디어 상태 관리: 검토중/채택/보류/기각 등 상태 태그
🛠️ 기술 스택
- 백엔드: Laravel 13.x (Eloquent ORM, SQLite)
- 프론트엔드: React 19, Inertia.js
- 스타일링: Tailwind CSS
- 아키텍처: 모노리스 풀스택 (별도 API 없음, Inertia::render()로 데이터 전달)
제약 조건
- • Laravel 13.x + Inertia.js + React 19 모노리스 구조 필수
- • npx create laravel 또는 Laravel Breeze (React + Inertia) starter kit 사용
- • 별도 RESTful API 라우트 없이 Inertia::render()로 컴포넌트에 데이터 전달
- • SQLite 데이터베이스 사용 (간편한 로컬 개발)
- • Tailwind CSS로 스타일링
- • 투표는 한 사용자당 한 아이디어에 1표만 (중간 테이블로 중복 방지)
- • 인증 시스템은 Laravel Breeze 기본 제공 사용 (로그인한 사용자만 아이디어 작성/투표)
- • 모든 CRUD 작업은 Inertia 폼 헬퍼(useForm) 활용하여 SPA 경험 제공
프롬프트 레시피
아래 프롬프트를 순서대로 AI에게 보내면 됩니다. 복사 버튼을 눌러 바로 사용하세요.
mkdir team-brainstorm-voting-board && cd team-brainstorm-voting-board 이 폴더에서 Laravel 13.x + React + Inertia.js 프로젝트를 시작합니다. 다음 명령으로 Laravel Breeze (React + Inertia) starter kit을 설치하세요: ```bash composer create-project laravel/laravel . composer require laravel/breeze --dev php artisan breeze:install react php artisan migrate npm install npm run dev ``` 설치가 완료되면 기본 인증 시스템(로그인/회원가입)이 구성됩니다. 다음으로 아이디어와 투표를 위한 데이터베이스 구조를 설계하세요: 1. **ideas 테이블** (아이디어 카드): - id (PK) - user_id (FK) - 작성자 - title (string, 100자) - description (text) - category (enum: 'feature_improvement', 'new_feature', 'ui_ux', 'other') - created_at, updated_at 2. **votes 테이블** (투표 기록, 중간 테이블): - id (PK) - user_id (FK) - idea_id (FK) - created_at, updated_at - UNIQUE 제약조건 (user_id, idea_id) - 한 사용자가 같은 아이디어에 중복 투표 방지 마이그레이션 파일 2개를 생성하고 스키마를 작성하세요. ```bash php artisan make:migration create_ideas_table php artisan make:migration create_votes_table ``` 각 마이그레이션 파일에 위 스키마를 구현하고 `php artisan migrate`로 테이블을 생성하세요. 마지막으로 Idea, Vote 모델을 생성하고 관계를 설정하세요: ```bash php artisan make:model Idea php artisan make:model Vote ``` - Idea 모델: `belongsTo(User)`, `hasMany(Vote)`, `votes_count` 접근자 추가 (withCount 활용) - Vote 모델: `belongsTo(User)`, `belongsTo(Idea)` - User 모델에 `hasMany(Idea)`, `hasMany(Vote)` 관계 추가 구현 완료 후 `php artisan serve`로 서버를 실행하고 http://localhost:8000 에서 로그인 페이지가 정상 작동하는지 확인하세요.
기대 결과: Laravel Breeze (React + Inertia) 프로젝트가 생성되고, ideas/votes 테이블 마이그레이션이 완료되며, Eloquent 모델 관계가 설정됩니다. 로그인/회원가입 화면이 정상 작동합니다.
이제 아이디어 목록을 보여주는 메인 대시보드 페이지를 만들겠습니다.
**백엔드 작업:**
1. IdeaController 생성:
```bash
php artisan make:controller IdeaController
```
2. `app/Http/Controllers/IdeaController.php`에 index 메서드 구현:
- 모든 아이디어를 votes_count(투표수)와 함께 가져오기 (`withCount('votes')`)
- 기본 정렬: votes_count 내림차순 (인기순)
- 각 아이디어에 작성자 정보(user)와 현재 로그인 사용자가 투표했는지 여부(`voted_by_user`) 포함
- `Inertia::render('Ideas/Index', ['ideas' => $ideas])`로 React 컴포넌트에 전달
3. `routes/web.php`에 라우트 추가:
```php
use App\Http\Controllers\IdeaController;
Route::middleware(['auth'])->group(function () {
Route::get('/ideas', [IdeaController::class, 'index'])->name('ideas.index');
});
```
**프론트엔드 작업:**
4. `resources/js/Pages/Ideas/Index.jsx` 컴포넌트 생성:
- props로 ideas 배열 받기
- Tailwind CSS로 그리드 레이아웃 (grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4)
- 각 아이디어를 카드 형태로 렌더링:
- 제목(title), 설명(description) 일부(100자 제한)
- 카테고리 배지 (색상별: 기능개선-파랑, 신규기능-초록, UI/UX-보라, 기타-회색)
- 투표수 표시 (👍 아이콘 + 숫자)
- 작성자 이름, 작성일
- 상단에 "새 아이디어 작성" 버튼 (나중에 구현)
5. 네비게이션에 Ideas 링크 추가:
- `resources/js/Layouts/AuthenticatedLayout.jsx`의 메뉴에 Ideas 링크 추가
구현 후 로그인하여 /ideas 페이지에서 빈 상태 또는 시드 데이터가 있다면 아이디어 카드가 그리드로 표시되는지 확인하세요.
테스트를 위해 tinker로 더미 데이터 몇 개를 생성해도 좋습니다:
```bash
php artisan tinker
>>> $user = User::first();
>>> App\Models\Idea::create(['user_id' => $user->id, 'title' => '다크모드 지원', 'description' => '사용자가 라이트/다크 테마를 전환할 수 있으면 좋겠습니다.', 'category' => 'ui_ux']);
```
기대 결과: IdeaController의 index 메서드가 아이디어 목록을 Inertia로 전달하고, Ideas/Index.jsx 컴포넌트가 카드 그리드 레이아웃으로 아이디어를 렌더링합니다. 네비게이션에 Ideas 링크가 추가되어 페이지 이동이 가능합니다.
아이디어 작성 기능을 추가하겠습니다.
**백엔드 작업:**
1. IdeaController에 create, store 메서드 추가:
- `create()`: 빈 폼을 렌더링 (`Inertia::render('Ideas/Create')`)
- `store(Request $request)`:
- 유효성 검사: title(required, max:100), description(required), category(required, in:feature_improvement,new_feature,ui_ux,other)
- 현재 로그인 사용자(auth()->user())가 작성자로 아이디어 생성
- 생성 후 `redirect()->route('ideas.index')`로 목록 페이지로 리다이렉트
- 성공 메시지 세션 플래시 (`session()->flash('success', '아이디어가 등록되었습니다!')`)
2. `routes/web.php`에 라우트 추가:
```php
Route::get('/ideas/create', [IdeaController::class, 'create'])->name('ideas.create');
Route::post('/ideas', [IdeaController::class, 'store'])->name('ideas.store');
```
**프론트엔드 작업:**
3. `resources/js/Pages/Ideas/Create.jsx` 컴포넌트 생성:
- Inertia의 useForm 훅 사용하여 폼 상태 관리:
```jsx
const { data, setData, post, processing, errors } = useForm({
title: '',
description: '',
category: 'feature_improvement'
});
```
- 폼 필드:
- title 입력 (input text)
- description 입력 (textarea, rows=5)
- category 선택 (select, 옵션: 기능개선/신규기능/UI-UX/기타)
- 제출 버튼: `post(route('ideas.store'))`로 전송
- 유효성 검사 에러 표시 (errors.title, errors.description 등)
- Tailwind CSS로 폼 스타일링 (max-w-2xl 중앙 정렬)
4. Ideas/Index.jsx에 "새 아이디어 작성" 버튼 구현:
- `<Link href={route('ideas.create')}>` 버튼 추가 (Inertia Link 컴포넌트 사용)
- 우측 상단에 배치 (+ 아이콘 포함)
5. 성공 메시지 표시:
- Ideas/Index.jsx에서 `usePage().props.flash` 확인하여 성공 메시지가 있으면 상단에 녹색 알림 배너 표시 (3초 후 자동 사라짐)
구현 후 "새 아이디어 작성" 버튼 클릭 → 폼 작성 → 제출 → 목록 페이지로 돌아와서 새 아이디어 카드가 추가되었는지 확인하세요.
기대 결과: 아이디어 작성 폼(Ideas/Create.jsx)이 동작하고, 제출 시 IdeaController의 store 메서드가 데이터를 저장한 후 목록 페이지로 리다이렉트합니다. 성공 메시지가 표시되고 새 아이디어가 목록에 나타납니다.
투표 기능을 구현하겠습니다.
**백엔드 작업:**
1. VoteController 생성:
```bash
php artisan make:controller VoteController
```
2. `app/Http/Controllers/VoteController.php`에 toggle 메서드 구현:
- 현재 로그인 사용자가 해당 아이디어에 투표했는지 확인
- 투표했다면 → 투표 취소 (Vote 레코드 삭제)
- 투표하지 않았다면 → 투표 추가 (Vote 레코드 생성)
- 성공 후 `redirect()->back()`으로 이전 페이지(목록 페이지)로 돌아가기
- 예외 처리: 이미 투표한 경우 중복 투표 방지 (try-catch로 unique 제약 위반 처리)
3. `routes/web.php`에 라우트 추가:
```php
use App\Http\Controllers\VoteController;
Route::post('/ideas/{idea}/vote', [VoteController::class, 'toggle'])->name('ideas.vote');
```
**프론트엔드 작업:**
4. Ideas/Index.jsx에서 투표 버튼 추가:
- 각 아이디어 카드에 투표 버튼 (👍 아이콘 + 투표수)
- Inertia의 useForm 또는 router.post 사용하여 투표 요청:
```jsx
import { router } from '@inertiajs/react';
const handleVote = (ideaId) => {
router.post(route('ideas.vote', ideaId), {}, {
preserveScroll: true
});
};
```
- 현재 사용자가 투표한 아이디어는 버튼 색상 변경 (예: 파란색 배경)
- 투표하지 않은 아이디어는 회색 배경
- 투표수 실시간 업데이트 (Inertia가 자동으로 페이지 데이터 갱신)
5. 카드 UI 개선:
- 투표 버튼을 카드 하단에 배치
- 호버 효과 추가 (hover:bg-blue-600 등)
- 로딩 중일 때 버튼 비활성화 (processing 상태 활용)
구현 후 아이디어 카드의 투표 버튼을 클릭하여 투표수가 증가하고, 다시 클릭하면 감소하는지 확인하세요. 페이지 새로고침 없이 부드럽게 동작해야 합니다.
기대 결과: VoteController의 toggle 메서드가 투표를 추가/취소하고, Ideas/Index.jsx의 투표 버튼이 클릭 시 Inertia를 통해 서버에 요청을 보냅니다. 투표수가 실시간으로 업데이트되고, 투표한 아이디어는 시각적으로 구분됩니다.
카테고리별 필터링과 정렬 기능을 추가하겠습니다.
**백엔드 작업:**
1. IdeaController의 index 메서드 수정:
- 쿼리 파라미터 받기: `category` (선택한 카테고리), `sort` (정렬 기준: popular/recent)
- category 파라미터가 있으면 해당 카테고리만 필터링 (`where('category', $category)`)
- sort 파라미터에 따라 정렬:
- 'popular' (기본값): `orderBy('votes_count', 'desc')`
- 'recent': `orderBy('created_at', 'desc')`
- 필터/정렬 옵션도 props로 전달:
```php
return Inertia::render('Ideas/Index', [
'ideas' => $ideas,
'filters' => [
'category' => $request->category,
'sort' => $request->sort ?? 'popular'
]
]);
```
**프론트엔드 작업:**
2. Ideas/Index.jsx에 필터/정렬 UI 추가:
- 상단에 탭 형태의 카테고리 필터:
- 전체 / 기능개선 / 신규기능 / UI-UX / 기타
- 각 탭 클릭 시 `router.get(route('ideas.index'), { category: '...' })`로 필터링
- 현재 선택된 탭 하이라이트 (배경색 변경)
- 정렬 드롭다운:
- 인기순 / 최신순 선택
- 변경 시 `router.get(route('ideas.index'), { sort: '...', category: filters.category })`로 요청
- Tailwind CSS로 탭과 드롭다운 스타일링
3. 필터/정렬 상태 유지:
- 쿼리 파라미터가 URL에 반영되도록 (`/ideas?category=ui_ux&sort=recent`)
- 페이지 이동 후에도 필터가 유지되도록 `preserveState: true` 옵션 사용
구현 후 카테고리 탭을 클릭하여 해당 카테고리 아이디어만 표시되는지, 정렬 드롭다운을 변경하여 정렬 순서가 바뀌는지 확인하세요.
기대 결과: IdeaController의 index 메서드가 category/sort 쿼리 파라미터를 처리하고, Ideas/Index.jsx에 카테고리 탭과 정렬 드롭다운이 추가됩니다. 필터/정렬이 URL에 반영되고 페이지 새로고침 없이 동작합니다.
아이디어 수정/삭제 기능을 추가하여 프로젝트를 완성하겠습니다.
**백엔드 작업:**
1. IdeaController에 edit, update, destroy 메서드 추가:
- `edit(Idea $idea)`:
- 본인이 작성한 아이디어만 수정 가능 (`$this->authorize('update', $idea)`)
- Policy 생성: `php artisan make:policy IdeaPolicy --model=Idea`
- IdeaPolicy의 update 메서드: `return $user->id === $idea->user_id;`
- `Inertia::render('Ideas/Edit', ['idea' => $idea])`
- `update(Request $request, Idea $idea)`:
- 권한 확인 후 유효성 검사
- 아이디어 업데이트
- 성공 메시지와 함께 목록 페이지로 리다이렉트
- `destroy(Idea $idea)`:
- 권한 확인 후 삭제
- 관련 투표(votes)도 자동 삭제 (cascade 또는 수동 삭제)
- 성공 메시지와 함께 목록 페이지로 리다이렉트
2. `routes/web.php`에 라우트 추가:
```php
Route::get('/ideas/{idea}/edit', [IdeaController::class, 'edit'])->name('ideas.edit');
Route::put('/ideas/{idea}', [IdeaController::class, 'update'])->name('ideas.update');
Route::delete('/ideas/{idea}', [IdeaController::class, 'destroy'])->name('ideas.destroy');
```
3. IdeaPolicy 등록:
- `app/Providers/AuthServiceProvider.php`의 policies 배열에 추가:
```php
protected $policies = [
Idea::class => IdeaPolicy::class,
];
```
**프론트엔드 작업:**
4. `resources/js/Pages/Ideas/Edit.jsx` 컴포넌트 생성:
- Create.jsx와 유사한 폼 구조
- useForm 초기값을 props.idea로 설정:
```jsx
const { data, setData, put, processing, errors } = useForm({
title: idea.title,
description: idea.description,
category: idea.category
});
```
- 제출 시 `put(route('ideas.update', idea.id))`
- 취소 버튼: `<Link href={route('ideas.index')}>` 목록으로 돌아가기
5. Ideas/Index.jsx에 수정/삭제 버튼 추가:
- 각 카드에 작성자 본인만 보이는 수정/삭제 버튼 (조건부 렌더링: `idea.user.id === auth.user.id`)
- 수정 버튼: `<Link href={route('ideas.edit', idea.id)}>` (연필 아이콘)
- 삭제 버튼: `router.delete(route('ideas.destroy', idea.id))` (휴지통 아이콘)
- 삭제 시 확인 다이얼로그 표시: `if (confirm('정말 삭제하시겠습니까?'))`
- 버튼을 카드 우측 상단에 배치 (아이콘만 표시)
6. 최종 UI 개선:
- 빈 상태 메시지: 아이디어가 없을 때 "아직 아이디어가 없습니다. 첫 번째 아이디어를 작성해보세요!" 표시
- 로딩 상태: Inertia의 progress 바 활용 (기본 제공)
- 반응형 디자인 확인: 모바일/태블릿/데스크톱에서 레이아웃이 적절히 조정되는지 테스트
구현 후 본인이 작성한 아이디어의 수정/삭제 버튼이 표시되고, 수정/삭제가 정상 작동하는지 확인하세요. 다른 사용자의 아이디어에는 버튼이 보이지 않아야 합니다.
최종적으로 다음을 테스트하세요:
1. 회원가입/로그인
2. 아이디어 작성
3. 다른 아이디어에 투표
4. 카테고리 필터링
5. 정렬 변경
6. 본인 아이디어 수정/삭제
모든 기능이 페이지 새로고침 없이 부드럽게 동작하면 성공입니다! 🎉
기대 결과: IdeaController의 edit, update, destroy 메서드와 IdeaPolicy가 구현되고, Ideas/Edit.jsx 컴포넌트가 작동합니다. 본인이 작성한 아이디어에만 수정/삭제 버튼이 표시되며, 모든 CRUD 기능이 완성됩니다. 최종 테스트를 통해 전체 흐름이 정상 작동함을 확인합니다.