diff --git a/src/app/workspaces/[workspaceId]/progress-chart/page.tsx b/src/app/workspaces/[workspaceId]/progress-chart/page.tsx
index 2aa5514..01aa47c 100644
--- a/src/app/workspaces/[workspaceId]/progress-chart/page.tsx
+++ b/src/app/workspaces/[workspaceId]/progress-chart/page.tsx
@@ -1,4 +1,5 @@
// 진행률 차트 라우트 — 워크스페이스 존재/용도만 서버에서 판정하고, 데이터 조회·조립은 client 컨테이너(useQuery)에 위임한다.
+// 선택 스프린트는 searchParam(?sprint=id)으로 읽어 client 컨테이너에 넘긴다(사이드 프로젝트 진행률에서만 사용).
import { ProgressChartPage } from '@/views/progress-chart';
import { assertWorkspaceRouteAccess } from '@/entities/workspace/lib/assert-workspace-route-access';
@@ -6,13 +7,17 @@ interface WorkspaceProgressChartPageProps {
params: Promise<{
workspaceId: string;
}>;
+ searchParams: Promise<{ sprint?: string | string[] }>;
}
export default async function WorkspaceProgressChartPage({
params,
+ searchParams,
}: WorkspaceProgressChartPageProps) {
const { workspaceId } = await params;
+ const { sprint: sprintParam } = await searchParams;
+ const selectedSprintId = typeof sprintParam === 'string' ? sprintParam : undefined;
await assertWorkspaceRouteAccess(workspaceId, 'progress-chart');
- return ;
+ return ;
}
diff --git a/src/entities/side-project/sprint/index.ts b/src/entities/side-project/sprint/index.ts
index f26429a..3c02c56 100644
--- a/src/entities/side-project/sprint/index.ts
+++ b/src/entities/side-project/sprint/index.ts
@@ -7,7 +7,13 @@ export { sprintsQueryKey, useSprints } from './api/use-sprints';
export { useCreateSprint } from './api/use-create-sprint';
export { useUpdateSprint } from './api/use-update-sprint';
export { useDeleteSprint } from './api/use-delete-sprint';
-export { resolveCurrentSprint, selectVelocity, selectVelocityMax } from './model/sprint.selectors';
+export {
+ resolveCurrentSprint,
+ resolveSelectedSprint,
+ selectVelocity,
+ selectVelocityMax,
+} from './model/sprint.selectors';
export { toSprint } from './model/sprint.mapper';
export type { SprintRow, SprintRpcRow } from './model/sprint.db.types';
export { sprintInputSchema, type SprintInput } from './model/sprint.schema';
+export { SprintSelector } from './ui/SprintSelector';
diff --git a/src/entities/side-project/sprint/model/sprint.selectors.ts b/src/entities/side-project/sprint/model/sprint.selectors.ts
index 2e1d8aa..8ce83f3 100644
--- a/src/entities/side-project/sprint/model/sprint.selectors.ts
+++ b/src/entities/side-project/sprint/model/sprint.selectors.ts
@@ -37,3 +37,15 @@ export function resolveCurrentSprint(sprints: Sprint[]): Sprint | undefined {
// 진행 중이 없으면 가장 최근 시작한 스프린트 (원본 불변)
return [...sprints].sort((a, b) => b.startDate.localeCompare(a.startDate))[0];
}
+
+/**
+ * 화면에 표시할 스프린트를 판정한다 — 선택 id(URL ?sprint=id 등)가 유효하면 그 스프린트,
+ * 없거나 유효하지 않으면 현재 스프린트로 폴백한다(진행 중 우선 → 없으면 최신).
+ * 스프린트 보드/진행률 차트가 공유하는 선택 규칙을 한 곳에 둔다.
+ */
+export function resolveSelectedSprint(
+ sprints: Sprint[],
+ selectedSprintId: string | undefined,
+): Sprint | undefined {
+ return sprints.find((sprint) => sprint.id === selectedSprintId) ?? resolveCurrentSprint(sprints);
+}
diff --git a/src/views/side-project/sprint-board/ui/SprintSelector.tsx b/src/entities/side-project/sprint/ui/SprintSelector.tsx
similarity index 80%
rename from src/views/side-project/sprint-board/ui/SprintSelector.tsx
rename to src/entities/side-project/sprint/ui/SprintSelector.tsx
index da67c18..38498dc 100644
--- a/src/views/side-project/sprint-board/ui/SprintSelector.tsx
+++ b/src/entities/side-project/sprint/ui/SprintSelector.tsx
@@ -1,16 +1,18 @@
// 스프린트 선택기 — URL 파라미터(?sprint=id)로 전환한다.
// 클릭 = 네비게이션 → 라우트 RSC가 다시 실행되어 선택 스프린트로 재조회/재seed된다(클라 페칭 없음).
+// 스프린트 보드/진행률 차트가 공유하는 순수 표현 컴포넌트라 entity 계층에 둔다.
import Link from 'next/link';
-import type { Sprint } from '@/entities/side-project/sprint';
import { cn } from '@/shared/lib/utils';
+import type { Sprint } from '../model/sprint.types';
+
interface SprintSelectorProps {
sprints: Sprint[];
currentSprintId: string;
}
-export default function SprintSelector({ sprints, currentSprintId }: SprintSelectorProps) {
+export function SprintSelector({ sprints, currentSprintId }: SprintSelectorProps) {
return (
{sprints.map((sprint) => {
diff --git a/src/views/progress-chart/ui/ProgressChartPage.tsx b/src/views/progress-chart/ui/ProgressChartPage.tsx
index 8161376..fbaa29d 100644
--- a/src/views/progress-chart/ui/ProgressChartPage.tsx
+++ b/src/views/progress-chart/ui/ProgressChartPage.tsx
@@ -6,14 +6,24 @@ import { notFound } from 'next/navigation';
interface ProgressChartPageProps {
workspaceId: string;
+ /** 선택 스프린트(?sprint=id) — 사이드 프로젝트 진행률에서만 사용, 없으면 현재 스프린트로 폴백 */
+ selectedSprintId?: string;
}
-export default async function ProgressChartPage({ workspaceId }: ProgressChartPageProps) {
+export default async function ProgressChartPage({
+ workspaceId,
+ selectedSprintId,
+}: ProgressChartPageProps) {
const workspace = await getWorkspaceById(workspaceId);
if (!workspace) return notFound();
if (workspace.purpose === 'side-project') {
- return
;
+ return (
+
+ );
}
return (
diff --git a/src/views/side-project/progress-chart/ui/ProgressChartView.tsx b/src/views/side-project/progress-chart/ui/ProgressChartView.tsx
index cf5b1a0..349bf8c 100644
--- a/src/views/side-project/progress-chart/ui/ProgressChartView.tsx
+++ b/src/views/side-project/progress-chart/ui/ProgressChartView.tsx
@@ -4,7 +4,12 @@
// 로딩/에러/빈 상태를 여기서 분기하고, 하위 차트 컴포넌트는 순수 표현만 담당한다.
import { Plus_Jakarta_Sans } from 'next/font/google';
-import { resolveCurrentSprint, selectVelocity, useSprints } from '@/entities/side-project/sprint';
+import {
+ resolveSelectedSprint,
+ selectVelocity,
+ SprintSelector,
+ useSprints,
+} from '@/entities/side-project/sprint';
import { countByStatus, useSprintTasks } from '@/entities/side-project/task';
import ProgressStatRow from './ProgressStatRow';
@@ -25,10 +30,18 @@ function CenteredMessage({ children }: { children: React.ReactNode }) {
);
}
-export function ProgressChartView({ workspaceId }: { workspaceId: string }) {
+interface ProgressChartViewProps {
+ workspaceId: string;
+ /** 선택 스프린트(?sprint=id) — 없거나 유효하지 않으면 현재 스프린트로 폴백 */
+ selectedSprintId?: string;
+}
+
+export function ProgressChartView({ workspaceId, selectedSprintId }: ProgressChartViewProps) {
const sprintsQuery = useSprints(workspaceId);
- // 진행 중(오늘이 기간 안) 스프린트 우선 → 없으면 최신. 로딩 중이면 undefined.
- const sprint = sprintsQuery.data ? resolveCurrentSprint(sprintsQuery.data) : undefined;
+ // 선택값이 없거나 유효하지 않으면 현재 스프린트로 폴백. 로딩 중이면 undefined.
+ const sprint = sprintsQuery.data
+ ? resolveSelectedSprint(sprintsQuery.data, selectedSprintId)
+ : undefined;
const tasksQuery = useSprintTasks(sprint?.id);
if (sprintsQuery.isPending) return
불러오는 중…;
@@ -46,6 +59,9 @@ export function ProgressChartView({ workspaceId }: { workspaceId: string }) {
return (
+ {/* 스프린트 선택기 — 상단/스탯/도넛은 선택 스프린트에 묶이고, 벨로시티는 전체 추이를 유지한다 */}
+
+
diff --git a/src/views/side-project/sprint-board/ui/SprintBoardView.tsx b/src/views/side-project/sprint-board/ui/SprintBoardView.tsx
index 0a544af..d1d5923 100644
--- a/src/views/side-project/sprint-board/ui/SprintBoardView.tsx
+++ b/src/views/side-project/sprint-board/ui/SprintBoardView.tsx
@@ -6,7 +6,7 @@
// members(담당자 표시명 해석용)는 RSC 값(initialMembers)으로 첫 렌더를 채우고 공유 캐시가 소유한다.
import { Plus_Jakarta_Sans } from 'next/font/google';
-import { resolveCurrentSprint, useSprints } from '@/entities/side-project/sprint';
+import { resolveSelectedSprint, SprintSelector, useSprints } from '@/entities/side-project/sprint';
import { useBacklogTasks, useSprintTasks } from '@/entities/side-project/task';
import {
useWorkspaceMembersByWorkspaceId,
@@ -16,7 +16,6 @@ import { SprintBoard } from '@/features/manage-sprint-tasks';
import { SprintToolbar } from '@/features/manage-sprints';
-import SprintSelector from './SprintSelector';
import SprintSummaryHeader from './SprintSummaryHeader';
const jakarta = Plus_Jakarta_Sans({
@@ -49,10 +48,9 @@ export function SprintBoardView({
initialMembers,
);
const sprintsQuery = useSprints(workspaceId);
- // 선택값이 없거나 유효하지 않으면 데이터에서 현재 스프린트를 판정(진행 중 우선 → 없으면 최신)
+ // 선택값이 없거나 유효하지 않으면 현재 스프린트로 폴백(진행 중 우선 → 없으면 최신)
const sprint = sprintsQuery.data
- ? (sprintsQuery.data.find((item) => item.id === selectedSprintId) ??
- resolveCurrentSprint(sprintsQuery.data))
+ ? resolveSelectedSprint(sprintsQuery.data, selectedSprintId)
: undefined;
const tasksQuery = useSprintTasks(sprint?.id);
const backlogQuery = useBacklogTasks(workspaceId);