+
{sprints.map((sprint) => {
const isActive = sprint.id === currentSprintId;
return (
diff --git a/src/widgets/side-project/dashboard-backlog/ui/Backlog.tsx b/src/widgets/side-project/dashboard-backlog/ui/Backlog.tsx
index 845bbbf..8a13e01 100644
--- a/src/widgets/side-project/dashboard-backlog/ui/Backlog.tsx
+++ b/src/widgets/side-project/dashboard-backlog/ui/Backlog.tsx
@@ -3,7 +3,7 @@
// · md/lg: 우선순위 점 + 항목 + 포인트 리스트(넘치면 스크롤)
// 워크스페이스의 백로그(스프린트 미편입) 업무를 셀렉터로 가져온다.
import { currentSprint } from '@/entities/side-project/sprint';
-import { getBacklogTasks, type Task, TASK_PRIORITY } from '@/entities/side-project/task';
+import { getMockBacklogTasks, type Task, TASK_PRIORITY } from '@/entities/side-project/task';
import type { WidgetSize } from '@/shared/dashboard/lib/widget-size';
import { WidgetCard, WidgetCardAction, WidgetCardHeader } from '@/shared/dashboard/ui/widget-card';
@@ -11,7 +11,7 @@ const header = (
보드} />
);
-const backlogItems: Task[] = getBacklogTasks(currentSprint.workspaceId);
+const backlogItems: Task[] = getMockBacklogTasks(currentSprint.workspaceId);
export default function Backlog({ size = 'md' }: { size?: WidgetSize }) {
if (size === 'sm') {
diff --git a/src/widgets/side-project/dashboard-my-tasks/ui/MyTasks.tsx b/src/widgets/side-project/dashboard-my-tasks/ui/MyTasks.tsx
index 9d783d4..b736b3f 100644
--- a/src/widgets/side-project/dashboard-my-tasks/ui/MyTasks.tsx
+++ b/src/widgets/side-project/dashboard-my-tasks/ui/MyTasks.tsx
@@ -4,7 +4,12 @@
// · lg: 상태별 카운트 요약 + task 리스트
// 현재 스프린트에 편입된 업무를 셀렉터로 가져온다(백로그는 애초에 포함되지 않음).
import { currentSprint } from '@/entities/side-project/sprint';
-import { getSprintTasks, Task, TASK_STATUS, type TaskStatus } from '@/entities/side-project/task';
+import {
+ getMockSprintTasks,
+ type Task,
+ TASK_STATUS,
+ type TaskStatus,
+} from '@/entities/side-project/task';
import type { WidgetSize } from '@/shared/dashboard/lib/widget-size';
import { WidgetCard, WidgetCardAction, WidgetCardHeader } from '@/shared/dashboard/ui/widget-card';
@@ -13,7 +18,7 @@ const header = (
);
// 현재 스프린트 편입 업무
-const sprintTasks: Task[] = getSprintTasks(currentSprint.id);
+const sprintTasks: Task[] = getMockSprintTasks(currentSprint.id);
const countBy = (status: TaskStatus) => sprintTasks.filter((task) => task.status === status).length;
export default function MyTasks({ size = 'md' }: { size?: WidgetSize }) {
diff --git a/src/widgets/side-project/dashboard-velocity/ui/Velocity.tsx b/src/widgets/side-project/dashboard-velocity/ui/Velocity.tsx
index 20916ef..62bed42 100644
--- a/src/widgets/side-project/dashboard-velocity/ui/Velocity.tsx
+++ b/src/widgets/side-project/dashboard-velocity/ui/Velocity.tsx
@@ -1,9 +1,11 @@
// 벨로시티 위젯 — 스프린트별 계획/완료 포인트를 막대로 비교
// 막대가 2그룹뿐이라 별도 차트 라이브러리 없이 순수 CSS(div height %)로 구현한다.
-import { sprintVelocity, VELOCITY_MAX } from '@/entities/side-project/sprint';
+import { mockSprints, selectVelocity, VELOCITY_MAX } from '@/entities/side-project/sprint';
import { WidgetCard, WidgetCardHeader } from '@/shared/dashboard/ui/widget-card';
export default function Velocity() {
+ const sprintVelocity = selectVelocity(mockSprints);
+
return (
diff --git a/supabase/migrations/20260712220809_create_sprint_rpcs.sql b/supabase/migrations/20260712220809_create_sprint_rpcs.sql
new file mode 100644
index 0000000..3b2ba34
--- /dev/null
+++ b/supabase/migrations/20260712220809_create_sprint_rpcs.sql
@@ -0,0 +1,42 @@
+-- sprint 도메인 RPC
+-- 공통: auth 연동 전이므로 p_workspace_id 파라미터로 스코프를 받는다 (연동 후 멤버십 검증은 RLS/auth.uid()에 위임)
+
+-- 워크스페이스의 스프린트 목록 — 카드/벨로시티에 필요한 집계를 단일 쿼리로 반환 (스프린트 수와 무관하게 쿼리 1회, N+1 없음)
+-- 파생값은 tasks에서 집계하며 sprints 테이블에 저장하지 않는다:
+-- total_points/completed_points = 해당 스프린트 tasks의 point 합(완료는 status='done' 필터)
+-- days_left = 마감일까지 남은 일수(지난 스프린트는 0)
+create or replace function public.get_sprints(p_workspace_id uuid)
+returns table (
+ id uuid,
+ workspace_id uuid,
+ name text,
+ start_date date,
+ end_date date,
+ total_points int,
+ completed_points int,
+ days_left int
+)
+language sql
+stable
+set search_path = public, pg_temp
+as $$
+ select
+ s.id,
+ s.workspace_id,
+ s.name,
+ s.start_date,
+ s.end_date,
+ p.total_points,
+ p.completed_points,
+ greatest(0, (s.end_date - current_date))::int as days_left
+ from sprints s
+ cross join lateral (
+ select
+ coalesce(sum(t.point), 0)::int as total_points,
+ coalesce(sum(t.point) filter (where t.status = 'done'), 0)::int as completed_points
+ from tasks t
+ where t.sprint_id = s.id
+ ) p
+ where s.workspace_id = p_workspace_id
+ order by s.start_date;
+$$;