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
14 changes: 12 additions & 2 deletions src/app/api/dashboard/detail/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,19 @@ import { NextResponse } from 'next/server';

import { getDashboardDetailTodos } from '@/shared/lib/customApi/getDashboardDetailTodos';

export async function GET() {
const parseGoalIds = (rawGoalIds: string | null): number[] => {
if (!rawGoalIds) return [];
return rawGoalIds
.split(',')
.map((value) => Number(value.trim()))
.filter((id) => Number.isInteger(id) && id > 0);
};

export async function GET(request: Request) {
try {
const result = await getDashboardDetailTodos();
const { searchParams } = new URL(request.url);
const goalIds = parseGoalIds(searchParams.get('goalIds'));
const result = await getDashboardDetailTodos(goalIds);

return NextResponse.json(result, {
status: result.hasAnySuccess ? 200 : 502,
Expand Down
21 changes: 16 additions & 5 deletions src/features/dashboard/components/DashboardDetail/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,38 @@ import Empty from '@/shared/components/Empty';
import PageSubTitle from '@/shared/components/PageSubTitle';
import GoalBox from '../GoalBox';

import { dashboardQueries } from '@/shared/lib/query/queryFunction';
import { dashboardQueries, goalQueries } from '@/shared/lib/query/queryFunction';
import { useTodoModeStore } from '@/shared/stores/useTodoModeStore';
import { useLanguage } from '@/shared/contexts/LanguageContext';
import { GITHUB_DISCONNECTED_SESSION_KEY } from '@/shared/constants/github';

export default function DashboardDetail() {
const mode = useTodoModeStore((state) => state.mode);
const { data: goalDetail } = useQuery(dashboardQueries.detailTodos());
const { data: goals, isFetched: isGoalsFetched } = useQuery(goalQueries.list({ limit: 100 }));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

goalQueries.list 호출 시 limit: 100으로 하드코딩되어 있습니다. 사용자의 목표(Goal) 개수가 100개를 초과할 경우 일부 목표가 대시보드에 표시되지 않을 수 있습니다. 시스템의 최대 목표 생성 제한이 없다면, 페이지네이션을 도입하거나 충분히 큰 값을 사용하도록 검토가 필요합니다.

const { t } = useLanguage();

const [isGithubDisconnectedSession] = useState(() => {
if (typeof window === 'undefined') return false;
return window.sessionStorage.getItem(GITHUB_DISCONNECTED_SESSION_KEY) === 'true';
});

const visibleGoals =
const visibleGoalIds =
mode === 'GITHUB' && isGithubDisconnectedSession
? []
: (goalDetail?.items?.filter((item) => item.goal.source === mode) ?? []);
: (goals?.goals?.filter((goal) => goal.source === mode).map((goal) => goal.id) ?? []);

if (mode === 'MANUAL' && visibleGoals.length === 0) {
const { data: goalDetail } = useQuery({
...dashboardQueries.detailTodosByGoals(visibleGoalIds),
enabled: visibleGoalIds.length > 0,
});

const visibleGoals = goalDetail?.items ?? [];

if (!isGoalsFetched) {
return null;
}

if (mode === 'MANUAL' && visibleGoalIds.length === 0) {
return <Empty>{t.dashboard.noFirstGoal}</Empty>;
}

Expand Down
10 changes: 5 additions & 5 deletions src/features/dashboard/components/TaskCardWrapper/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useQuery } from '@tanstack/react-query';
import { motion } from 'framer-motion';
import { useEffect, useState } from 'react';

Check warning on line 3 in src/features/dashboard/components/TaskCardWrapper/index.tsx

View workflow job for this annotation

GitHub Actions / test

'useEffect' is defined but never used

import TaskCard from '@/shared/components/TaskCard';

Expand Down Expand Up @@ -60,13 +60,13 @@
error instanceof ApiError
? error.message
: todoDetail.type === 'ISSUE'
? 'GitHub Issue close�� �����߽��ϴ�. ��� �� �ٽ� �õ����ּ���.'
: 'GitHub PR merge�� �����߽��ϴ�. ��� �� �ٽ� �õ����ּ���.';
? 'GitHub Issue close에 실패했습니다. 다시 시도해주세요.'
: 'GitHub PR merge에 실패했습니다. 다시 시도해주세요.';
showToast(message, 'fail');
} else {
showToast('���� ���� ������Ʈ�� �����߽��ϴ�.', 'fail');
showToast('할 일 상태 업데이트에 실패했습니다.', 'fail');
}
console.error('���� ���� ������Ʈ ����:', error);
console.error('할 일 상태 업데이트 오류:', error);
}
};

Expand All @@ -83,7 +83,7 @@
showToast(nextStarred ? t.mutations.favoriteAdded : t.mutations.favoriteRemoved);
},
onError: (error) => {
console.error(error);
console.error('즐겨찾기 상태 업데이트 오류:', error);
setStarred(!nextStarred);
},
});
Expand Down
11 changes: 9 additions & 2 deletions src/shared/lib/api/fetchDashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,15 @@ class FetchDashboard {
return payload.data;
};

getDashboardDetailTodos = async (): Promise<{ items: DashboardDetailTodosResponse[] }> => {
const response = await fetch('/api/dashboard/detail', {
getDashboardDetailTodos = async (goalIds?: number[]): Promise<{ items: DashboardDetailTodosResponse[] }> => {
const params = new URLSearchParams();
const normalizedGoalIds = (goalIds ?? []).filter((id) => Number.isInteger(id) && id > 0);
if (normalizedGoalIds.length > 0) {
params.set('goalIds', normalizedGoalIds.join(','));
}

const requestUrl = params.size > 0 ? `/api/dashboard/detail?${params.toString()}` : '/api/dashboard/detail';
const response = await fetch(requestUrl, {
method: 'GET',
headers: {
Accept: 'application/json',
Expand Down
21 changes: 16 additions & 5 deletions src/shared/lib/customApi/getDashboardDetailTodos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,25 @@ const toProgressPercent = (completedCount: number, todoCount: number): number =>
return Math.round((completedCount / todoCount) * 100);
};

export const getDashboardDetailTodos = async (): Promise<DashboardDetailTodosResult> => {
const normalizeGoalIds = (goalIds?: number[]): number[] => {
if (!goalIds) return [];
return [...new Set(goalIds.filter((id) => Number.isInteger(id) && id > 0))];
};

export const getDashboardDetailTodos = async (goalIds?: number[]): Promise<DashboardDetailTodosResult> => {
const targetGoalIds = normalizeGoalIds(goalIds);
const targetGoalIdSet = new Set(targetGoalIds);
const todoFetchLimit = Math.max(SAFE_LIMIT * Math.max(targetGoalIds.length, 1), SAFE_LIMIT * 5);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

todoFetchLimit 계산 로직이 변경되면서 기존(300개)보다 조회하는 할 일(Todo)의 수가 크게 줄어들 수 있습니다. (예: 목표가 지정되지 않았거나 1개일 때 50개만 조회). 이 함수는 전체 할 일 목록을 최신순으로 가져온 뒤 메모리에서 필터링하므로, 특정 목표의 할 일이 전체 최신 목록의 상위 todoFetchLimit 범위를 벗어나면 대시보드에서 해당 데이터가 누락되어 보일 수 있습니다. 데이터 정합성을 위해 limit을 상향하거나 목표별 조회가 가능한지 확인이 필요합니다.


const [goalsRes, openTodosRes, doneTodosRes] = await Promise.allSettled([
fetchGoals.getGoals(),
fetchTodos.getTodos({ sort: 'LATEST', search: '', limit: 300, done: false }),
fetchTodos.getTodos({ sort: 'LATEST', search: '', limit: 300, done: true }),
fetchGoals.getGoals({ limit: Math.max(targetGoalIds.length, 50) }),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

fetchGoals.getGoalslimitMath.max(targetGoalIds.length, 50)으로 설정되어 있어 버그가 발생할 가능성이 있습니다. 클라이언트(DashboardDetail)에서는 최대 100개의 목표 ID를 보낼 수 있는데, 만약 요청된 ID가 서버의 전체 목표 목록 중 상위 50개 이후에 위치한다면 해당 목표 정보를 가져오지 못하게 됩니다. 클라이언트에서 보낼 수 있는 최대 범위(현재 100개)를 고려하여 limit을 설정해야 합니다.

Suggested change
fetchGoals.getGoals({ limit: Math.max(targetGoalIds.length, 50) }),
fetchGoals.getGoals({ limit: Math.max(targetGoalIds.length, 100) }),

fetchTodos.getTodos({ sort: 'LATEST', search: '', limit: todoFetchLimit, done: false }),
fetchTodos.getTodos({ sort: 'LATEST', search: '', limit: todoFetchLimit, done: true }),
]);

const goals = goalsRes.status === 'fulfilled' ? goalsRes.value.goals : [];
const allGoals = goalsRes.status === 'fulfilled' ? goalsRes.value.goals : [];
const goals =
targetGoalIds.length > 0 ? allGoals.filter((goal) => targetGoalIdSet.has(goal.id)) : allGoals;
const openTodos = openTodosRes.status === 'fulfilled' ? openTodosRes.value.todos : [];
const doneTodos = doneTodosRes.status === 'fulfilled' ? doneTodosRes.value.todos : [];

Expand Down
1 change: 1 addition & 0 deletions src/shared/lib/query/keyFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,5 @@ export const dashboardKeys = {
all: ['dashboard'] as const,
summary: () => [...dashboardKeys.all, 'summary'] as const,
detailTodos: () => [...dashboardKeys.all, 'detailTodos'] as const,
detailTodosByGoals: (goalIds: number[]) => [...dashboardKeys.detailTodos(), goalIds] as const,
};
7 changes: 7 additions & 0 deletions src/shared/lib/query/queryFunction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,4 +177,11 @@ export const dashboardQueries = {
queryFn: () => fetchDashboard.getDashboardDetailTodos(),
staleTime: DASHBOARD_STALE_TIME,
}),

detailTodosByGoals: (goalIds: number[]) =>
queryOptions({
queryKey: dashboardKeys.detailTodosByGoals(goalIds),
queryFn: () => fetchDashboard.getDashboardDetailTodos(goalIds),
staleTime: DASHBOARD_STALE_TIME,
}),
};
Loading