팀 스프린트 번다운 차트 - 실시간 작업 진행률 시각화
sprint-burndown-chart-tracker
문제 설명
프로젝트 개요
애자일 개발팀을 위한 스프린트 번다운 차트 트래커를 만듭니다. 팀원들이 스프린트 기간 동안 작업(Task)을 등록하고 완료 처리하면, 자동으로 번다운 차트가 그려지며 실시간으로 진행 상황을 시각화합니다.
필수 기능
- 스프린트 생성: 스프린트명, 시작일, 종료일, 총 스토리 포인트 설정
- 작업(Task) 관리: 작업 제목, 담당자, 스토리 포인트, 상태(To Do/In Progress/Done) CRUD
- 번다운 차트: Chart.js를 활용해 이상적인 번다운 라인(Ideal Line)과 실제 번다운 라인(Actual Line)을 시각화
- 일별 스냅샷: 매일 남은 스토리 포인트를 기록하여 차트 데이터 생성
- 필터링: 스프린트별, 담당자별 작업 필터링
- 진행률 표시: 완료된 스토리 포인트 / 총 스토리 포인트 퍼센트 표시
선택 추가 기능 (보너스)
- 작업 상태 변경 시 자동으로 일별 스냅샷 업데이트
- 스프린트 종료 시 회고 메모 작성 기능
- 여러 스프린트 비교 차트 (속도 개선 추이)
- 담당자별 기여도 차트 (완료한 스토리 포인트 합계)
- 드래그 앤 드롭으로 작업 상태 변경 (Kanban 스타일)
기술 스택
- 백엔드: Laravel 13.x (Eloquent, 마이그레이션, 컨트롤러)
- 프론트엔드: React 19 + Inertia.js (모노리스 풀스택)
- 데이터베이스: SQLite (개발 편의성)
- 차트 라이브러리: Chart.js (react-chartjs-2)
- 스타일링: Tailwind CSS
학습 목표
- Laravel + Inertia.js + React 통합 개발 경험
- 복잡한 데이터 관계 설계 (Sprint, Task, DailySnapshot)
- Chart.js를 활용한 데이터 시각화
- 날짜 기반 데이터 처리 및 계산 로직
- React 컴포넌트 분리 및 props 전달 패턴
제약 조건
- • Laravel 13.x + Inertia.js + React 19 모노리스 풀스택 구조 (별도 API 엔드포인트 없음)
- • Laravel Breeze (React + Inertia) starter kit으로 프로젝트 생성
- • 데이터베이스는 SQLite 사용 (config/database.php 기본 설정)
- • Chart.js (react-chartjs-2) 라이브러리로 번다운 차트 구현
- • Tailwind CSS로 스타일링
- • 모델 관계: Sprint (1) - (N) Task, Sprint (1) - (N) DailySnapshot
- • DailySnapshot은 매일 자정 또는 작업 완료 시 자동 생성 (스케줄러 또는 이벤트 활용)
- • 번다운 차트는 이상적인 라인(시작 포인트 → 0까지 선형 감소)과 실제 라인(DailySnapshot 기반) 동시 표시
- • 작업 상태는 'todo', 'in_progress', 'done' 3가지
- • React 컴포넌트는 Pages/ 폴더에 페이지별로 구성, 재사용 컴포넌트는 Components/ 폴더에 분리
프롬프트 레시피
아래 프롬프트를 순서대로 AI에게 보내면 됩니다. 복사 버튼을 눌러 바로 사용하세요.
mkdir sprint-burndown-chart-tracker && cd sprint-burndown-chart-tracker 이 폴더에서 프로젝트를 시작합니다. Laravel 13.x + Inertia.js + React 19 스타터 프로젝트를 생성해주세요. ```bash npx create-laravel sprint-burndown-chart-tracker --breeze --stack=react --typescript=no cd sprint-burndown-chart-tracker ``` 또는 Laravel Breeze를 수동 설치: ```bash composer create-project laravel/laravel . composer require laravel/breeze --dev php artisan breeze:install react npm install npm run build ``` 데이터베이스는 SQLite를 사용합니다. `.env` 파일에서 다음과 같이 수정: ``` DB_CONNECTION=sqlite # DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORD 줄은 주석 처리 또는 삭제 ``` `database/database.sqlite` 파일 생성: ```bash touch database/database.sqlite ``` 마이그레이션 실행: ```bash php artisan migrate ``` 개발 서버 실행: ```bash php artisan serve ``` 별도 터미널에서 Vite 개발 서버 실행: ```bash npm run dev ``` http://localhost:8000 접속하여 Laravel Breeze 기본 화면이 나오는지 확인해주세요.
기대 결과: Laravel 13.x + Breeze (React + Inertia) 프로젝트가 생성되고, SQLite 데이터베이스가 설정되며, 기본 인증 화면이 정상적으로 표시됩니다. `php artisan serve`와 `npm run dev`가 모두 실행 중이어야 합니다.
스프린트와 작업을 관리할 데이터베이스 모델과 마이그레이션을 생성해주세요.
## 필요한 모델과 테이블
### 1. Sprint 모델
- 필드: id, name (string), start_date (date), end_date (date), total_story_points (integer), created_at, updated_at
- 관계: hasMany Task, hasMany DailySnapshot
### 2. Task 모델
- 필드: id, sprint_id (foreign key), title (string), assignee (string, nullable), story_points (integer), status (enum: 'todo', 'in_progress', 'done', default 'todo'), created_at, updated_at
- 관계: belongsTo Sprint
### 3. DailySnapshot 모델
- 필드: id, sprint_id (foreign key), snapshot_date (date, unique per sprint), remaining_story_points (integer), created_at, updated_at
- 관계: belongsTo Sprint
- 용도: 매일 남은 스토리 포인트를 기록하여 번다운 차트 데이터로 사용
다음 명령어로 모델과 마이그레이션을 생성:
```bash
php artisan make:model Sprint -m
php artisan make:model Task -m
php artisan make:model DailySnapshot -m
```
각 마이그레이션 파일을 위 스키마에 맞게 작성하고, 모델 파일에 관계 메서드를 추가해주세요.
예시 (Sprint 모델):
```php
class Sprint extends Model
{
protected $fillable = ['name', 'start_date', 'end_date', 'total_story_points'];
protected $casts = [
'start_date' => 'date',
'end_date' => 'date',
];
public function tasks()
{
return $this->hasMany(Task::class);
}
public function dailySnapshots()
{
return $this->hasMany(DailySnapshot::class);
}
}
```
마이그레이션 실행:
```bash
php artisan migrate
```
완료 후 `php artisan tinker`로 Sprint 모델이 정상적으로 로드되는지 확인해주세요.
기대 결과: Sprint, Task, DailySnapshot 모델과 마이그레이션이 생성되고, 데이터베이스 테이블이 생성됩니다. 각 모델에 관계 메서드가 정의되어 있고, `php artisan tinker`에서 모델을 불러올 수 있습니다.
스프린트 관리 기능을 구현해주세요.
## Sprint CRUD
### 1. 라우트 (routes/web.php)
```php
use App\Http\Controllers\SprintController;
Route::middleware(['auth'])->group(function () {
Route::get('/sprints', [SprintController::class, 'index'])->name('sprints.index');
Route::get('/sprints/create', [SprintController::class, 'create'])->name('sprints.create');
Route::post('/sprints', [SprintController::class, 'store'])->name('sprints.store');
Route::get('/sprints/{sprint}', [SprintController::class, 'show'])->name('sprints.show');
Route::get('/sprints/{sprint}/edit', [SprintController::class, 'edit'])->name('sprints.edit');
Route::put('/sprints/{sprint}', [SprintController::class, 'update'])->name('sprints.update');
Route::delete('/sprints/{sprint}', [SprintController::class, 'destroy'])->name('sprints.destroy');
});
```
### 2. SprintController 생성
```bash
php artisan make:controller SprintController --resource
```
SprintController에 다음 메서드 구현:
- **index**: 모든 스프린트 목록 조회 (Inertia::render('Sprints/Index'))
- **create**: 스프린트 생성 폼 (Inertia::render('Sprints/Create'))
- **store**: 스프린트 저장 (유효성 검증: name required, start_date/end_date date 형식, total_story_points integer)
- **show**: 스프린트 상세 + 작업 목록 + 번다운 차트 데이터 (Inertia::render('Sprints/Show'))
- **edit**: 스프린트 수정 폼 (Inertia::render('Sprints/Edit'))
- **update**: 스프린트 업데이트
- **destroy**: 스프린트 삭제
### 3. React 페이지 컴포넌트 (resources/js/Pages/Sprints/)
- **Index.jsx**: 스프린트 목록 테이블 (이름, 기간, 총 포인트, 액션 버튼)
- **Create.jsx**: 스프린트 생성 폼 (name, start_date, end_date, total_story_points 입력)
- **Show.jsx**: 스프린트 상세 페이지 (다음 단계에서 작업 목록과 차트 추가 예정, 현재는 스프린트 정보만 표시)
- **Edit.jsx**: 스프린트 수정 폼
각 페이지는 `@inertiajs/react`의 `useForm` 훅을 사용하여 폼 데이터 관리 및 제출.
Tailwind CSS로 스타일링하고, 네비게이션 메뉴에 "Sprints" 링크 추가 (resources/js/Layouts/AuthenticatedLayout.jsx).
완료 후 브라우저에서 스프린트를 생성/조회/수정/삭제할 수 있는지 확인해주세요.
기대 결과: SprintController와 Sprint CRUD 라우트가 생성되고, React 페이지 컴포넌트(Index, Create, Show, Edit)가 구현됩니다. 브라우저에서 스프린트를 생성하고 목록에서 확인할 수 있으며, 수정 및 삭제가 정상 작동합니다.
스프린트 상세 페이지에 작업(Task) 관리 기능을 추가해주세요.
## Task CRUD (스프린트 상세 페이지 내 구현)
### 1. TaskController 생성
```bash
php artisan make:controller TaskController
```
TaskController에 다음 메서드 구현:
- **store**: 작업 생성 (sprint_id, title, assignee, story_points, status 저장)
- **update**: 작업 수정 (주로 status 변경, title/assignee/story_points 수정 가능)
- **destroy**: 작업 삭제
### 2. 라우트 추가 (routes/web.php)
```php
use App\Http\Controllers\TaskController;
Route::middleware(['auth'])->group(function () {
Route::post('/sprints/{sprint}/tasks', [TaskController::class, 'store'])->name('tasks.store');
Route::put('/tasks/{task}', [TaskController::class, 'update'])->name('tasks.update');
Route::delete('/tasks/{task}', [TaskController::class, 'destroy'])->name('tasks.destroy');
});
```
### 3. SprintController의 show 메서드 수정
스프린트 상세 조회 시 관련 작업 목록도 함께 전달:
```php
public function show(Sprint $sprint)
{
$sprint->load('tasks');
return Inertia::render('Sprints/Show', [
'sprint' => $sprint,
]);
}
```
### 4. Sprints/Show.jsx 수정
- 작업 목록 테이블 표시 (제목, 담당자, 스토리 포인트, 상태)
- 작업 추가 폼 (인라인 또는 모달)
- 작업 상태 변경 버튼 (To Do → In Progress → Done)
- 작업 삭제 버튼
### 5. 재사용 컴포넌트 생성
- **resources/js/Components/TaskList.jsx**: 작업 목록 테이블 컴포넌트
- **resources/js/Components/TaskForm.jsx**: 작업 추가/수정 폼 컴포넌트
작업 상태 변경 시 `useForm`의 `put` 메서드로 `/tasks/{task}` 엔드포인트 호출.
완료 후 스프린트 상세 페이지에서 작업을 추가하고, 상태를 변경하고, 삭제할 수 있는지 확인해주세요.
기대 결과: TaskController와 Task CRUD 라우트가 생성되고, Sprints/Show.jsx에 작업 목록과 추가/수정/삭제 기능이 구현됩니다. 스프린트 상세 페이지에서 작업을 관리할 수 있으며, 상태 변경이 즉시 반영됩니다.
일별 스냅샷(DailySnapshot) 자동 생성 로직을 구현해주세요.
## DailySnapshot 생성 전략
작업 상태가 'done'으로 변경될 때마다, 해당 스프린트의 오늘 날짜 스냅샷을 업데이트합니다.
### 1. Task 모델에 이벤트 리스너 추가
Task 모델에 `updated` 이벤트 리스너를 추가하여 상태 변경 시 스냅샷 업데이트:
```php
// app/Models/Task.php
protected static function booted()
{
static::updated(function ($task) {
if ($task->isDirty('status')) {
$task->sprint->updateDailySnapshot();
}
});
}
```
### 2. Sprint 모델에 updateDailySnapshot 메서드 추가
```php
// app/Models/Sprint.php
public function updateDailySnapshot()
{
$today = now()->toDateString();
$remainingPoints = $this->tasks()->where('status', '!=', 'done')->sum('story_points');
DailySnapshot::updateOrCreate(
[
'sprint_id' => $this->id,
'snapshot_date' => $today,
],
[
'remaining_story_points' => $remainingPoints,
]
);
}
```
### 3. 스프린트 생성 시 초기 스냅샷 생성
SprintController의 `store` 메서드에서 스프린트 생성 후 시작일에 초기 스냅샷 생성:
```php
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'start_date' => 'required|date',
'end_date' => 'required|date|after_or_equal:start_date',
'total_story_points' => 'required|integer|min:1',
]);
$sprint = Sprint::create($validated);
// 초기 스냅샷 생성 (시작일에 총 포인트)
DailySnapshot::create([
'sprint_id' => $sprint->id,
'snapshot_date' => $sprint->start_date,
'remaining_story_points' => $sprint->total_story_points,
]);
return redirect()->route('sprints.index');
}
```
### 4. 테스트
- 새 스프린트를 생성하고, 작업을 추가한 후 상태를 'done'으로 변경
- `php artisan tinker`에서 DailySnapshot 데이터 확인:
```php
DailySnapshot::all();
```
완료 후 작업 완료 시 자동으로 스냅샷이 생성/업데이트되는지 확인해주세요.
기대 결과: Task 모델에 이벤트 리스너가 추가되고, Sprint 모델에 updateDailySnapshot 메서드가 구현됩니다. 작업 상태를 'done'으로 변경하면 자동으로 오늘 날짜의 DailySnapshot이 생성되거나 업데이트됩니다. 데이터베이스에서 스냅샷 데이터를 확인할 수 있습니다.
번다운 차트를 구현해주세요.
## Chart.js 설치 및 설정
### 1. Chart.js 및 react-chartjs-2 설치
```bash
npm install chart.js react-chartjs-2
```
### 2. SprintController의 show 메서드 수정
스프린트 상세 조회 시 번다운 차트 데이터도 함께 전달:
```php
public function show(Sprint $sprint)
{
$sprint->load(['tasks', 'dailySnapshots' => function ($query) {
$query->orderBy('snapshot_date');
}]);
// 이상적인 번다운 라인 계산
$startDate = \Carbon\Carbon::parse($sprint->start_date);
$endDate = \Carbon\Carbon::parse($sprint->end_date);
$totalDays = $startDate->diffInDays($endDate) + 1;
$dailyIdealBurn = $sprint->total_story_points / ($totalDays - 1);
$idealLine = [];
for ($i = 0; $i < $totalDays; $i++) {
$date = $startDate->copy()->addDays($i)->toDateString();
$remaining = max(0, $sprint->total_story_points - ($dailyIdealBurn * $i));
$idealLine[] = ['date' => $date, 'remaining' => round($remaining, 1)];
}
// 실제 번다운 라인 (DailySnapshot 기반)
$actualLine = $sprint->dailySnapshots->map(function ($snapshot) {
return [
'date' => $snapshot->snapshot_date,
'remaining' => $snapshot->remaining_story_points,
];
});
return Inertia::render('Sprints/Show', [
'sprint' => $sprint,
'idealLine' => $idealLine,
'actualLine' => $actualLine,
]);
}
```
### 3. BurndownChart 컴포넌트 생성
**resources/js/Components/BurndownChart.jsx**:
```jsx
import React from 'react';
import { Line } from 'react-chartjs-2';
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
} from 'chart.js';
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend
);
export default function BurndownChart({ idealLine, actualLine }) {
const labels = idealLine.map(point => point.date);
const data = {
labels,
datasets: [
{
label: 'Ideal Burndown',
data: idealLine.map(point => point.remaining),
borderColor: 'rgba(59, 130, 246, 1)', // blue
backgroundColor: 'rgba(59, 130, 246, 0.2)',
borderDash: [5, 5],
},
{
label: 'Actual Burndown',
data: actualLine.map(point => {
const found = idealLine.find(ideal => ideal.date === point.date);
return found ? point.remaining : null;
}),
borderColor: 'rgba(239, 68, 68, 1)', // red
backgroundColor: 'rgba(239, 68, 68, 0.2)',
spanGaps: true,
},
],
};
const options = {
responsive: true,
plugins: {
legend: {
position: 'top',
},
title: {
display: true,
text: 'Sprint Burndown Chart',
},
},
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: 'Remaining Story Points',
},
},
x: {
title: {
display: true,
text: 'Date',
},
},
},
};
return <Line data={data} options={options} />;
}
```
### 4. Sprints/Show.jsx에 차트 추가
```jsx
import BurndownChart from '@/Components/BurndownChart';
export default function Show({ sprint, idealLine, actualLine }) {
return (
<AuthenticatedLayout>
<div className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
<h1 className="text-2xl font-bold mb-4">{sprint.name}</h1>
<p>Start: {sprint.start_date} | End: {sprint.end_date}</p>
<p>Total Story Points: {sprint.total_story_points}</p>
<div className="mt-8">
<BurndownChart idealLine={idealLine} actualLine={actualLine} />
</div>
{/* 작업 목록 (이전 단계에서 구현) */}
<TaskList tasks={sprint.tasks} sprintId={sprint.id} />
</div>
</AuthenticatedLayout>
);
}
```
완료 후 스프린트 상세 페이지에서 번다운 차트가 표시되는지 확인하고, 작업을 완료할 때마다 실제 라인이 업데이트되는지 테스트해주세요.
기대 결과: Chart.js가 설치되고, BurndownChart 컴포넌트가 생성됩니다. Sprints/Show.jsx에 차트가 표시되며, 이상적인 번다운 라인(파란색 점선)과 실제 번다운 라인(빨간색 실선)이 그려집니다. 작업 완료 시 실제 라인이 업데이트됩니다.