From 2c9f43a7cb4f0dbdbaa438f966b60f125e27839f Mon Sep 17 00:00:00 2001 From: Kwon812 Date: Tue, 21 Jul 2026 09:42:39 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=EC=8A=A4=ED=94=84=EB=A6=B0?= =?UTF-8?q?=ED=8A=B8=20=EC=84=A0=ED=83=9D=20=ED=95=84=ED=84=B0=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20SprintSelector=EA=B3=B5=ED=86=B5?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../[workspaceId]/progress-chart/page.tsx | 7 ++++- src/entities/side-project/sprint/index.ts | 1 + .../sprint}/ui/SprintSelector.tsx | 6 +++-- .../progress-chart/ui/ProgressChartPage.tsx | 14 ++++++++-- .../progress-chart/ui/ProgressChartView.tsx | 26 ++++++++++++++++--- .../sprint-board/ui/SprintBoardView.tsx | 3 +-- 6 files changed, 46 insertions(+), 11 deletions(-) rename src/{views/side-project/sprint-board => entities/side-project/sprint}/ui/SprintSelector.tsx (80%) diff --git a/src/app/workspaces/[workspaceId]/progress-chart/page.tsx b/src/app/workspaces/[workspaceId]/progress-chart/page.tsx index a6b5722..c2742e2 100644 --- a/src/app/workspaces/[workspaceId]/progress-chart/page.tsx +++ b/src/app/workspaces/[workspaceId]/progress-chart/page.tsx @@ -1,16 +1,21 @@ // 진행률 차트 라우트 — 워크스페이스 존재/용도만 서버에서 판정하고, 데이터 조회·조립은 client 컨테이너(useQuery)에 위임한다. +// 선택 스프린트는 searchParam(?sprint=id)으로 읽어 client 컨테이너에 넘긴다(사이드 프로젝트 진행률에서만 사용). import { ProgressChartPage } from '@/views/progress-chart'; 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; - return ; + return ; } diff --git a/src/entities/side-project/sprint/index.ts b/src/entities/side-project/sprint/index.ts index f26429a..305830e 100644 --- a/src/entities/side-project/sprint/index.ts +++ b/src/entities/side-project/sprint/index.ts @@ -11,3 +11,4 @@ export { resolveCurrentSprint, selectVelocity, selectVelocityMax } from './model 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/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..8260076 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 { + resolveCurrentSprint, + selectVelocity, + SprintSelector, + useSprints, +} from '@/entities/side-project/sprint'; import { countByStatus, useSprintTasks } from '@/entities/side-project/task'; import ProgressStatRow from './ProgressStatRow'; @@ -25,10 +30,20 @@ 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 + ? (sprintsQuery.data.find((item) => item.id === selectedSprintId) ?? + resolveCurrentSprint(sprintsQuery.data)) + : undefined; const tasksQuery = useSprintTasks(sprint?.id); if (sprintsQuery.isPending) return 불러오는 중…; @@ -46,6 +61,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..a9bcbce 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 { resolveCurrentSprint, 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({ From 3516baba124011049cf6310d3a5f52948f614209 Mon Sep 17 00:00:00 2001 From: Kwon812 Date: Tue, 21 Jul 2026 10:07:06 +0900 Subject: [PATCH 2/2] =?UTF-8?q?=20refactor:=20=EC=84=A0=ED=83=9D=20?= =?UTF-8?q?=EC=8A=A4=ED=94=84=EB=A6=B0=ED=8A=B8=20=ED=8C=90=EC=A0=95=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20resolveSelectedSprint=EB=A1=9C=20=EA=B3=B5?= =?UTF-8?q?=ED=86=B5=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/side-project/sprint/index.ts | 7 ++++++- .../side-project/sprint/model/sprint.selectors.ts | 12 ++++++++++++ .../progress-chart/ui/ProgressChartView.tsx | 8 +++----- .../side-project/sprint-board/ui/SprintBoardView.tsx | 7 +++---- 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/entities/side-project/sprint/index.ts b/src/entities/side-project/sprint/index.ts index 305830e..3c02c56 100644 --- a/src/entities/side-project/sprint/index.ts +++ b/src/entities/side-project/sprint/index.ts @@ -7,7 +7,12 @@ 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'; 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/progress-chart/ui/ProgressChartView.tsx b/src/views/side-project/progress-chart/ui/ProgressChartView.tsx index 8260076..349bf8c 100644 --- a/src/views/side-project/progress-chart/ui/ProgressChartView.tsx +++ b/src/views/side-project/progress-chart/ui/ProgressChartView.tsx @@ -5,7 +5,7 @@ import { Plus_Jakarta_Sans } from 'next/font/google'; import { - resolveCurrentSprint, + resolveSelectedSprint, selectVelocity, SprintSelector, useSprints, @@ -38,11 +38,9 @@ interface ProgressChartViewProps { export function ProgressChartView({ workspaceId, selectedSprintId }: ProgressChartViewProps) { const sprintsQuery = useSprints(workspaceId); - // 선택값이 없거나 유효하지 않으면 데이터에서 현재 스프린트를 판정(진행 중 우선 → 없으면 최신). - // 로딩 중이면 undefined. + // 선택값이 없거나 유효하지 않으면 현재 스프린트로 폴백. 로딩 중이면 undefined. const sprint = sprintsQuery.data - ? (sprintsQuery.data.find((item) => item.id === selectedSprintId) ?? - resolveCurrentSprint(sprintsQuery.data)) + ? resolveSelectedSprint(sprintsQuery.data, selectedSprintId) : undefined; const tasksQuery = useSprintTasks(sprint?.id); diff --git a/src/views/side-project/sprint-board/ui/SprintBoardView.tsx b/src/views/side-project/sprint-board/ui/SprintBoardView.tsx index a9bcbce..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, SprintSelector, 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, @@ -48,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);