Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/app/workspaces/[workspaceId]/progress-chart/page.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
// 진행률 차트 라우트 — 워크스페이스 존재/용도만 서버에서 판정하고, 데이터 조회·조립은 client 컨테이너(useQuery)에 위임한다.
// 선택 스프린트는 searchParam(?sprint=id)으로 읽어 client 컨테이너에 넘긴다(사이드 프로젝트 진행률에서만 사용).
import { ProgressChartPage } from '@/views/progress-chart';
import { assertWorkspaceRouteAccess } from '@/entities/workspace/lib/assert-workspace-route-access';

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 <ProgressChartPage workspaceId={workspaceId} />;
return <ProgressChartPage workspaceId={workspaceId} selectedSprintId={selectedSprintId} />;
}
8 changes: 7 additions & 1 deletion src/entities/side-project/sprint/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
12 changes: 12 additions & 0 deletions src/entities/side-project/sprint/model/sprint.selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex flex-wrap gap-2">
{sprints.map((sprint) => {
Expand Down
14 changes: 12 additions & 2 deletions src/views/progress-chart/ui/ProgressChartPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <SideProjectProgressChartView workspaceId={workspaceId} />;
return (
<SideProjectProgressChartView
workspaceId={workspaceId}
selectedSprintId={selectedSprintId}
/>
);
}
return (
<div className={`${plusJakartaSans.className} bg-brand-surface min-h-full`}>
Expand Down
24 changes: 20 additions & 4 deletions src/views/side-project/progress-chart/ui/ProgressChartView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 <CenteredMessage>불러오는 중…</CenteredMessage>;
Expand All @@ -46,6 +59,9 @@ export function ProgressChartView({ workspaceId }: { workspaceId: string }) {
return (
<div className={`${jakarta.className} bg-brand-surface min-h-full`}>
<div className="flex flex-col gap-4">
{/* 스프린트 선택기 — 상단/스탯/도넛은 선택 스프린트에 묶이고, 벨로시티는 전체 추이를 유지한다 */}
<SprintSelector sprints={sprintsQuery.data} currentSprintId={sprint.id} />

<ProgressStatRow sprint={sprint} />

<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
Expand Down
8 changes: 3 additions & 5 deletions src/views/side-project/sprint-board/ui/SprintBoardView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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({
Expand Down Expand Up @@ -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);
Expand Down