diff --git a/src/app/(main)/calendar/page.tsx b/src/app/(main)/calendar/page.tsx index 3b654cb6..c9e421d9 100644 --- a/src/app/(main)/calendar/page.tsx +++ b/src/app/(main)/calendar/page.tsx @@ -1,5 +1,5 @@ import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'; -import { todoQueries, goalQueries, userQueries } from '@/shared/lib/query/queryKeys'; +import { todoQueries, goalQueries, userQueries } from '@/shared/lib/query/queryFunction'; import CalendarClient from '@/features/calendar/components/CalendarClient'; import { DataBoundary } from '@/shared/components/ErrorSuspenseBoundary'; diff --git a/src/app/(main)/dashboard/all-todo/page.tsx b/src/app/(main)/dashboard/all-todo/page.tsx index 478b426a..512b2286 100644 --- a/src/app/(main)/dashboard/all-todo/page.tsx +++ b/src/app/(main)/dashboard/all-todo/page.tsx @@ -2,7 +2,7 @@ import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query import AllTodoContent from '@/features/dashboard/allTodo/components/AllTodoContent'; -import { todoQueries } from '@/shared/lib/query/queryKeys'; +import { todoQueries } from '@/shared/lib/query/queryFunction'; export const dynamic = 'force-dynamic'; diff --git a/src/app/(main)/dashboard/favorite-todo/page.tsx b/src/app/(main)/dashboard/favorite-todo/page.tsx index b15ed558..8b4dfd1c 100644 --- a/src/app/(main)/dashboard/favorite-todo/page.tsx +++ b/src/app/(main)/dashboard/favorite-todo/page.tsx @@ -2,7 +2,7 @@ import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query import FavoriteTodoContent from '@/features/dashboard/favorite-todo/components/FavoriteTodoContent'; -import { todoQueries } from '@/shared/lib/query/queryKeys'; +import { todoQueries } from '@/shared/lib/query/queryFunction'; export const dynamic = 'force-dynamic'; diff --git a/src/app/(main)/dashboard/page.tsx b/src/app/(main)/dashboard/page.tsx index 382cb517..9eaa5c2e 100644 --- a/src/app/(main)/dashboard/page.tsx +++ b/src/app/(main)/dashboard/page.tsx @@ -5,13 +5,13 @@ import DashboardDetail from '@/features/dashboard/components/DashboardDetail'; import DashboardDetailSkeleton from '@/features/dashboard/components/DashboardDetailSkeleton'; import { DataBoundary } from '@/shared/components/ErrorSuspenseBoundary'; -import { dashboardQueries } from '@/shared/lib/query/queryKeys'; +import { dashboardQueries } from '@/shared/lib/query/queryFunction'; export const dynamic = 'force-dynamic'; /** * @description 해당 페이지는 서버 컴포넌트입니다. 클라이언트 컴포넌트로 변경하지 말아주세요 - * 'use client'로 변경 x + * 'use client'로 변경x */ export default async function DashboardPage() { diff --git a/src/app/(main)/goal/[goalId]/note/@modal/(.)[noteId]/page.tsx b/src/app/(main)/goal/[goalId]/note/@modal/(.)[noteId]/page.tsx index a97d7a6b..e17dba9e 100644 --- a/src/app/(main)/goal/[goalId]/note/@modal/(.)[noteId]/page.tsx +++ b/src/app/(main)/goal/[goalId]/note/@modal/(.)[noteId]/page.tsx @@ -1,5 +1,5 @@ import { dehydrate, QueryClient, HydrationBoundary } from '@tanstack/react-query'; -import { noteQueries, goalQueries } from '@/shared/lib/query/queryKeys'; +import { noteQueries, goalQueries } from '@/shared/lib/query/queryFunction'; import NoteDetailClient from '@/features/note/components/NoteDetailClient'; import { notFound } from 'next/navigation'; import NoteDetailModal from '@/features/note/components/NoteDetailModal'; diff --git a/src/app/(main)/goal/[goalId]/note/[noteId]/(detail)/page.tsx b/src/app/(main)/goal/[goalId]/note/[noteId]/(detail)/page.tsx index 5fd20993..f0caf63d 100644 --- a/src/app/(main)/goal/[goalId]/note/[noteId]/(detail)/page.tsx +++ b/src/app/(main)/goal/[goalId]/note/[noteId]/(detail)/page.tsx @@ -1,5 +1,5 @@ import { dehydrate, QueryClient, HydrationBoundary } from '@tanstack/react-query'; -import { noteQueries, goalQueries } from '@/shared/lib/query/queryKeys'; +import { noteQueries, goalQueries } from '@/shared/lib/query/queryFunction'; import NoteDetailClient from '@/features/note/components/NoteDetailClient'; import { notFound } from 'next/navigation'; diff --git a/src/app/(main)/goal/[goalId]/note/[noteId]/edit/page.tsx b/src/app/(main)/goal/[goalId]/note/[noteId]/edit/page.tsx index df300c6c..3144f1de 100644 --- a/src/app/(main)/goal/[goalId]/note/[noteId]/edit/page.tsx +++ b/src/app/(main)/goal/[goalId]/note/[noteId]/edit/page.tsx @@ -1,5 +1,5 @@ import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'; -import { noteQueries, goalQueries, todoQueries } from '@/shared/lib/query/queryKeys'; +import { noteQueries, goalQueries, todoQueries } from '@/shared/lib/query/queryFunction'; import NoteEditClient from '@/features/note/components/NoteEditClient'; import { notFound } from 'next/navigation'; diff --git a/src/app/(main)/goal/[goalId]/note/page.tsx b/src/app/(main)/goal/[goalId]/note/page.tsx index 272effbd..551c092c 100644 --- a/src/app/(main)/goal/[goalId]/note/page.tsx +++ b/src/app/(main)/goal/[goalId]/note/page.tsx @@ -1,5 +1,5 @@ import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'; -import { noteQueries, goalQueries } from '@/shared/lib/query/queryKeys'; +import { noteQueries, goalQueries } from '@/shared/lib/query/queryFunction'; import NoteListContainer from '@/features/note/components/NoteListContainer'; import { DataBoundary } from '@/shared/components/ErrorSuspenseBoundary'; diff --git a/src/app/(main)/goal/[goalId]/page.tsx b/src/app/(main)/goal/[goalId]/page.tsx index 87718f01..2618b95a 100644 --- a/src/app/(main)/goal/[goalId]/page.tsx +++ b/src/app/(main)/goal/[goalId]/page.tsx @@ -3,7 +3,7 @@ import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query import GoalSummary from '@/features/goal/components/GoalSummary'; import GoalDetail from '@/features/goal/components/GoalDetail'; -import { userQueries, goalQueries } from '@/shared/lib/query/queryKeys'; +import { userQueries, goalQueries } from '@/shared/lib/query/queryFunction'; /** * @description 해당 페이지는 서버 컴포넌트입니다. 클라이언트 컴포넌트로 변경하지 말아주세요 diff --git a/src/app/(main)/mypage/MyPageForm.tsx b/src/app/(main)/mypage/MyPageForm.tsx index 8f2289fe..3e1587c6 100644 --- a/src/app/(main)/mypage/MyPageForm.tsx +++ b/src/app/(main)/mypage/MyPageForm.tsx @@ -9,7 +9,7 @@ import Button from '@/shared/components/Button'; import FormField from '@/shared/components/FormField'; import LoadingSpinner from '@/shared/components/LoadingSpinner'; -import { userQueries } from '@/shared/lib/query/queryKeys'; +import { userQueries } from '@/shared/lib/query/queryFunction'; import { useDeleteGithubConnection, usePatchCurrentUser, @@ -186,7 +186,7 @@ export default function MyPageForm() {
{!isMobile && } -
+
{/* 프로필 이미지 */}
@@ -241,7 +241,9 @@ export default function MyPageForm() { onChange={handleNicknameChange} placeholder={t.mypage.nicknamePlaceholder} /> - {nicknameSuccess &&

{t.mypage.nicknameSuccess}

} + {nicknameSuccess && ( +

{t.mypage.nicknameSuccess}

+ )} {/* 비밀번호 변경 - 소셜 로그인이면 숨김 */} @@ -279,7 +281,7 @@ export default function MyPageForm() { @@ -316,7 +318,7 @@ export default function MyPageForm() { diff --git a/src/app/api/dashboard/detail/route.ts b/src/app/api/dashboard/detail/route.ts new file mode 100644 index 00000000..9534e376 --- /dev/null +++ b/src/app/api/dashboard/detail/route.ts @@ -0,0 +1,18 @@ +import { NextResponse } from 'next/server'; + +import { getDashboardDetailTodos } from '@/shared/lib/customApi/getDashboardDetailTodos'; + +export async function GET() { + try { + const result = await getDashboardDetailTodos(); + + return NextResponse.json(result, { + status: result.hasAnySuccess ? 200 : 502, + headers: { + 'Cache-Control': 'no-store', + }, + }); + } catch { + return NextResponse.json({ message: 'Failed to fetch dashboard detail todos' }, { status: 502 }); + } +} diff --git a/src/app/api/dashboard/summary/route.ts b/src/app/api/dashboard/summary/route.ts index 3b09a7d6..64a5cc88 100644 --- a/src/app/api/dashboard/summary/route.ts +++ b/src/app/api/dashboard/summary/route.ts @@ -1,10 +1,10 @@ import { NextResponse } from 'next/server'; -import { fetchDashboard } from '@/shared/lib/api/fetchDashboard'; +import { getDashboardSummaryResult } from '@/shared/lib/customApi/getDashboardSummaryResult'; export async function GET() { try { - const result = await fetchDashboard.getDashboardSummaryResult(); + const result = await getDashboardSummaryResult(); return NextResponse.json(result, { status: result.hasAnySuccess ? 200 : 502, diff --git a/src/features/calendar/hooks/useCalendar.ts b/src/features/calendar/hooks/useCalendar.ts index 8c25b1d6..edd00dc7 100644 --- a/src/features/calendar/hooks/useCalendar.ts +++ b/src/features/calendar/hooks/useCalendar.ts @@ -2,7 +2,7 @@ import { useState } from 'react'; import { useQuery } from '@tanstack/react-query'; -import { todoQueries, goalQueries, userQueries } from '@/shared/lib/query/queryKeys'; +import { todoQueries, goalQueries, userQueries } from '@/shared/lib/query/queryFunction'; export function useCalendar() { const today = new Date(); diff --git a/src/features/dashboard/allTodo/components/AllTodoContent/index.tsx b/src/features/dashboard/allTodo/components/AllTodoContent/index.tsx index d472c167..e0a09502 100644 --- a/src/features/dashboard/allTodo/components/AllTodoContent/index.tsx +++ b/src/features/dashboard/allTodo/components/AllTodoContent/index.tsx @@ -9,7 +9,7 @@ import { DataBoundary } from '@/shared/components/ErrorSuspenseBoundary'; import PageHeader from '@/shared/components/PageHeader'; import TaskCardWrapper from '@/features/dashboard/components/TaskCardWrapper'; -import { goalQueries, todoQueries } from '@/shared/lib/query/queryKeys'; +import { goalQueries, todoQueries } from '@/shared/lib/query/queryFunction'; import { useTodoCreateModal } from '@/features/todo/hooks/useTodoCreateModal'; import type { TodoListResponse } from '@/shared/lib/api'; import { TodoOptions } from '@/shared/types/types'; @@ -40,7 +40,7 @@ export default function AllTodoContent() {
{allTodos.length === 0 ? ( -
+
{t.allTodo.empty}
) : ( @@ -94,7 +94,7 @@ function AllTodoFilter({ todos, selectedFilter, setSelectedFilter }: AllTodoFilt
@@ -149,7 +149,7 @@ function AllTodoFetcher({ todos, fetchNextPage, hasNextPage, isFetchingNextPage }, [hasNextPage, isFetchingNextPage, fetchNextPage]); return ( -
+
{t.allTodo.issueReopenNotice1} {t.allTodo.issueReopenNotice2} diff --git a/src/features/dashboard/components/DashboardDetail/index.tsx b/src/features/dashboard/components/DashboardDetail/index.tsx index 6e2f15dd..2f0679cc 100644 --- a/src/features/dashboard/components/DashboardDetail/index.tsx +++ b/src/features/dashboard/components/DashboardDetail/index.tsx @@ -8,16 +8,14 @@ import Empty from '@/shared/components/Empty'; import PageSubTitle from '@/shared/components/PageSubTitle'; import GoalBox from '../GoalBox'; -import { GoalListResponse } from '@/shared/lib/api'; - -import { goalQueries } from '@/shared/lib/query/queryKeys'; +import { dashboardQueries } 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: goals } = useQuery(goalQueries.list()); + const { data: goalDetail } = useQuery(dashboardQueries.detailTodos()); const { t } = useLanguage(); const [isGithubDisconnectedSession] = useState(() => { @@ -28,7 +26,7 @@ export default function DashboardDetail() { const visibleGoals = mode === 'GITHUB' && isGithubDisconnectedSession ? [] - : (goals?.goals?.filter((goal) => goal.source === mode) ?? []); + : (goalDetail?.items?.filter((item) => item.goal.source === mode) ?? []); if (mode === 'MANUAL' && visibleGoals.length === 0) { return {t.dashboard.noFirstGoal}; @@ -45,8 +43,8 @@ export default function DashboardDetail() { icons={Goal Icon} />
- {visibleGoals.map((goal) => ( - + {visibleGoals.map((goalDetailItem) => ( + ))}
@@ -54,11 +52,3 @@ export default function DashboardDetail() {
); } - -function GoalDetailItem({ goal }: { goal: GoalListResponse['goals'][number] }) { - const { data: goalDetail } = useQuery(goalQueries.detail(goal.id)); - - if (!goalDetail) return null; - - return ; -} diff --git a/src/features/dashboard/components/DashboardSummary/index.tsx b/src/features/dashboard/components/DashboardSummary/index.tsx index 11fa8c53..ede71376 100644 --- a/src/features/dashboard/components/DashboardSummary/index.tsx +++ b/src/features/dashboard/components/DashboardSummary/index.tsx @@ -13,7 +13,7 @@ import TaskCardWrapper from '../TaskCardWrapper'; import { useBreakpoint } from '@/shared/hooks/useBreakPoint'; import { useGithubRepoConnectModal } from '@/shared/hooks/useGithubRepoConnectModal'; -import { dashboardQueries } from '@/shared/lib/query/queryKeys'; +import { dashboardQueries } from '@/shared/lib/query/queryFunction'; import { useTodoModeStore, TodoMode } from '@/shared/stores/useTodoModeStore'; import { useLanguage } from '@/shared/contexts/LanguageContext'; import { DashboardSummaryResponse } from '@/shared/types/api/schemas/api.process'; diff --git a/src/features/dashboard/components/GoalBox/index.tsx b/src/features/dashboard/components/GoalBox/index.tsx index 8c5db989..db7b4d8f 100644 --- a/src/features/dashboard/components/GoalBox/index.tsx +++ b/src/features/dashboard/components/GoalBox/index.tsx @@ -1,10 +1,10 @@ 'use client'; -import { useState, useRef, useEffect } from 'react'; +import { useState } from 'react'; import { AnimatePresence } from 'framer-motion'; import { PlusIcon } from 'lucide-react'; import { useRouter } from 'next/navigation'; -import { useInfiniteQuery, useQuery, keepPreviousData } from '@tanstack/react-query'; +import { useQuery, keepPreviousData } from '@tanstack/react-query'; import Button from '@/shared/components/Button'; import Empty from '@/shared/components/Empty'; @@ -12,15 +12,16 @@ import Progressbar from '@/shared/components/Progressbar'; import SearchInput from '@/shared/components/SearchInput'; import TaskCardWrapper from '../TaskCardWrapper'; -import type { GoalDetailResponse } from '@/shared/lib/api'; import { useTodoCreateModal } from '@/features/todo/hooks/useTodoCreateModal'; import { useGithubTodoCreateModal } from '@/features/todo/hooks/useGithubTodoCreateModal'; import { useLanguage } from '@/shared/contexts/LanguageContext'; -import { todoQueries } from '@/shared/lib/query/queryKeys'; +import { todoQueries } from '@/shared/lib/query/queryFunction'; import { useDebounce } from '@/shared/hooks/useDebounce'; +import { DashboardDetailTodosResponse } from '@/shared/types/api/schemas/api.process'; +import { TodoResponse } from '@/shared/lib/api'; interface GoalBoxProps { - data: GoalDetailResponse; + data: DashboardDetailTodosResponse; } export default function GoalBox({ data }: GoalBoxProps) { const { openTodoCreateModal } = useTodoCreateModal(); @@ -30,9 +31,10 @@ export default function GoalBox({ data }: GoalBoxProps) { const [search, setSearch] = useState(''); const debouncedSearch = useDebounce(search.trim(), 300); const isSearching = debouncedSearch.length > 0; + const goalId = data.goal.id; const { data: searchResult } = useQuery({ - ...todoQueries.list({ sort: 'LATEST', search: debouncedSearch, goalId: data.id }), + ...todoQueries.list({ sort: 'LATEST', search: debouncedSearch, goalId }), placeholderData: keepPreviousData, enabled: isSearching, }); @@ -40,22 +42,20 @@ export default function GoalBox({ data }: GoalBoxProps) { const searchTodoItems = isSearching ? (searchResult?.todos.filter((todo) => !todo.done) ?? null) : null; const searchDoneItems = isSearching ? (searchResult?.todos.filter((todo) => todo.done) ?? null) : null; - const isGithubGoal = data.source === 'GITHUB'; + const isGithubGoal = data.goal.source === 'GITHUB'; const handleAddTodo = () => { - if (data.id === undefined) return; - if (isGithubGoal) { openGithubTodoCreateModal({ - goalId: data.id, - goalTitle: data.title, + goalId, + goalTitle: data.goal.title, }); } else { openTodoCreateModal({ - goalDetailId: data.id, + goalDetailId: goalId, todo: { title: '', - goalId: data.id, + goalId, dueDate: undefined, linkUrl: undefined, imageUrl: undefined, @@ -68,7 +68,7 @@ export default function GoalBox({ data }: GoalBoxProps) { const noSearchResults = isSearching && searchTodoItems?.length === 0 && searchDoneItems?.length === 0; return ( -
+
@@ -82,7 +82,6 @@ export default function GoalBox({ data }: GoalBoxProps) {
- +
); @@ -148,38 +146,13 @@ function GoalName({ data }: GoalNameProps) { interface ListBoxProps { title: string; mode: 'todo' | 'done'; - goalId: number; - searchItems: { id: number; favorite?: boolean }[] | null; + defaultItems: TodoResponse[]; + searchItems: TodoResponse[] | null; } -function ListBox({ title, mode, goalId, searchItems }: ListBoxProps) { +function ListBox({ title, mode, defaultItems, searchItems }: ListBoxProps) { const bgColor = mode === 'todo' ? 'bg-[#E5F9F2] dark:bg-gray-750' : 'bg-white dark:bg-gray-750'; const textColor = mode === 'todo' ? 'text-[#00D185]' : 'text-gray-400'; - const isDone = mode === 'done'; - const sentinelRef = useRef(null); - - const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({ - ...todoQueries.infiniteList({ goalId, done: isDone, limit: 10 }), - enabled: searchItems === null, - }); - - const infiniteItems = data?.pages.flatMap((p) => p.todos ?? []) ?? []; - const items = searchItems ?? infiniteItems; - - useEffect(() => { - if (searchItems !== null) return; - const sentinel = sentinelRef.current; - if (!sentinel) return; - const observer = new IntersectionObserver( - ([entry]) => { - if (entry.isIntersecting && hasNextPage && !isFetchingNextPage) { - fetchNextPage(); - } - }, - { threshold: 0.1 }, - ); - observer.observe(sentinel); - return () => observer.disconnect(); - }, [searchItems, hasNextPage, isFetchingNextPage, fetchNextPage]); + const items = searchItems ?? defaultItems; return (
{items.map((item) => ( - + ))} - {searchItems === null &&
} - {isFetchingNextPage && ( -
...
- )}
diff --git a/src/features/dashboard/components/TaskCardWrapper/index.tsx b/src/features/dashboard/components/TaskCardWrapper/index.tsx index f914e4a8..5d104e6a 100644 --- a/src/features/dashboard/components/TaskCardWrapper/index.tsx +++ b/src/features/dashboard/components/TaskCardWrapper/index.tsx @@ -1,36 +1,38 @@ import { useQuery } from '@tanstack/react-query'; import { motion } from 'framer-motion'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import TaskCard from '@/shared/components/TaskCard'; -import { ApiError } from '@/shared/lib/api'; +import { ApiError, TodoResponse } from '@/shared/lib/api'; import { usePatchTodo, usePatchTodoFavorite } from '@/shared/lib/query/mutations'; -import { todoQueries } from '@/shared/lib/query/queryKeys'; +import { todoQueries } from '@/shared/lib/query/queryFunction'; import { useToastStore } from '@/shared/stores/useToastStore'; import { useLanguage } from '@/shared/contexts/LanguageContext'; export default function TaskCardWrapper({ item, + todo, mode, }: { item: { id: number; favorite?: boolean }; + todo?: TodoResponse; mode: 'todo' | 'done'; }) { const { showToast } = useToastStore(); const { t } = useLanguage(); // 할 일 상세 정보를 가져오기 위한 query 훅 - const { data: todoDetail } = useQuery({ + const { data: queriedTodoDetail } = useQuery({ ...todoQueries.detail(item.id), - enabled: !!item.id, + enabled: !todo && !!item.id, }); + const todoDetail = todo ?? queriedTodoDetail; // 할 일 상태 업데이트를 위한 mutation 훅 const patchTodo = usePatchTodo(item.id); const handleCheckboxClick = async () => { if (todoDetail?.id === undefined) return; - // GitHub 연동 todo는 완료 후 되돌리기 불가 — 백엔드는 source를 "github"(소문자)로 반환 const isGithubTodo = todoDetail.source === 'github'; if (isGithubTodo && todoDetail.done) return; @@ -58,18 +60,18 @@ export default function TaskCardWrapper({ 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); } }; - // 할 일 즐겨찾기 토글을 위한 mutation 훅 - const [starred, setStarred] = useState(item?.favorite ?? false); + const [starred, setStarred] = useState(todoDetail?.favorite ?? item?.favorite ?? false); + const { mutate: patchTodoFavorite } = usePatchTodoFavorite(item?.id); const handleStarToggle = () => { @@ -102,7 +104,7 @@ export default function TaskCardWrapper({ > diff --git a/src/features/dashboard/favorite-todo/components/FavoriteTodoContent/index.tsx b/src/features/dashboard/favorite-todo/components/FavoriteTodoContent/index.tsx index 3102f365..d56361c4 100644 --- a/src/features/dashboard/favorite-todo/components/FavoriteTodoContent/index.tsx +++ b/src/features/dashboard/favorite-todo/components/FavoriteTodoContent/index.tsx @@ -10,7 +10,7 @@ import PageHeader from '@/shared/components/PageHeader'; import TaskCardWrapper from '@/features/dashboard/components/TaskCardWrapper'; import FavoriteTodoDropdownGoal from '../FavoriteTodoDropdownGoal'; -import { goalQueries, todoQueries } from '@/shared/lib/query/queryKeys'; +import { goalQueries, todoQueries } from '@/shared/lib/query/queryFunction'; import { useTodoCreateModal } from '@/features/todo/hooks/useTodoCreateModal'; import type { TodoListResponse } from '@/shared/lib/api'; import { TodoOptions } from '@/shared/types/types'; @@ -41,15 +41,9 @@ export default function FavoriteTodoContent() { return (
- {breakpoint !== 'mobile' && ( - - )} + {breakpoint !== 'mobile' && }
- + +
setSelectedGoal(item.value)} diff --git a/src/features/goal/components/DetailTodoModal/DetailTodoModalComponents/index.tsx b/src/features/goal/components/DetailTodoModal/DetailTodoModalComponents/index.tsx index e20fe90e..b3776583 100644 --- a/src/features/goal/components/DetailTodoModal/DetailTodoModalComponents/index.tsx +++ b/src/features/goal/components/DetailTodoModal/DetailTodoModalComponents/index.tsx @@ -9,7 +9,7 @@ import Tag from '@/shared/components/Tag'; import { useModalStore } from '@/shared/stores/useModalStore'; import { TodoResponse } from '@/shared/lib/api'; -import { noteQueries } from '@/shared/lib/query/queryKeys'; +import { noteQueries } from '@/shared/lib/query/queryFunction'; import { formatDate } from '@/shared/utils/utils'; import { useLanguage } from '@/shared/contexts/LanguageContext'; @@ -33,7 +33,7 @@ const DetailTodoModalComponents = memo(function DetailTodoModalComponents({ todo const hasGithubLink = githubSourceLabel && todo.linkUrl; return ( -
+
{todo?.title} @@ -42,7 +42,7 @@ const DetailTodoModalComponents = memo(function DetailTodoModalComponents({ todo
{/* GitHub 소스 뱃지 */} {githubSourceLabel && ( - + {githubSourceLabel} )} @@ -69,7 +69,7 @@ const DetailTodoModalComponents = memo(function DetailTodoModalComponents({ todo {githubSourceLabel && (
GitHub 연동 -
+
{hasGithubLink ? ( 노트 이미지 {note?.title} diff --git a/src/features/goal/components/DetailTodoModal/index.tsx b/src/features/goal/components/DetailTodoModal/index.tsx index 8b985493..752d979c 100644 --- a/src/features/goal/components/DetailTodoModal/index.tsx +++ b/src/features/goal/components/DetailTodoModal/index.tsx @@ -3,7 +3,7 @@ import { useQuery } from '@tanstack/react-query'; import DetailTodoModalComponents from './DetailTodoModalComponents'; -import { todoQueries } from '@/shared/lib/query/queryKeys'; +import { todoQueries } from '@/shared/lib/query/queryFunction'; import { memo } from 'react'; interface DetailTodoModalProps { diff --git a/src/features/goal/components/GoalDetail/index.tsx b/src/features/goal/components/GoalDetail/index.tsx index 8f1fcfe5..0df8f93f 100644 --- a/src/features/goal/components/GoalDetail/index.tsx +++ b/src/features/goal/components/GoalDetail/index.tsx @@ -8,7 +8,7 @@ import Button from '@/shared/components/Button'; import Empty from '@/shared/components/Empty'; import TaskCardWrapper from '@/features/dashboard/components/TaskCardWrapper'; -import { goalQueries } from '@/shared/lib/query/queryKeys'; +import { goalQueries } from '@/shared/lib/query/queryFunction'; import { useTodoCreateModal } from '@/features/todo/hooks/useTodoCreateModal'; import { useGithubTodoCreateModal } from '@/features/todo/hooks/useGithubTodoCreateModal'; import { useLanguage } from '@/shared/contexts/LanguageContext'; diff --git a/src/features/goal/components/GoalSummary/index.tsx b/src/features/goal/components/GoalSummary/index.tsx index 1f67ce56..800d6b8a 100644 --- a/src/features/goal/components/GoalSummary/index.tsx +++ b/src/features/goal/components/GoalSummary/index.tsx @@ -14,7 +14,7 @@ import SinglePostModal from '@/shared/components/Modal/SinglePostModal'; import { GoalDetailResponse } from '@/shared/lib/api'; import { useDeleteGoal, useDisconnectGithubGoal, usePatchGoal } from '@/shared/lib/query/mutations'; -import { goalQueries, userQueries } from '@/shared/lib/query/queryKeys'; +import { goalQueries, userQueries } from '@/shared/lib/query/queryFunction'; import { useBreakpoint } from '@/shared/hooks/useBreakPoint'; import { useModalStore } from '@/shared/stores/useModalStore'; import { useLanguage } from '@/shared/contexts/LanguageContext'; diff --git a/src/features/note/components/NoteCreateModal/index.tsx b/src/features/note/components/NoteCreateModal/index.tsx index a6770203..a71fbfa6 100644 --- a/src/features/note/components/NoteCreateModal/index.tsx +++ b/src/features/note/components/NoteCreateModal/index.tsx @@ -6,7 +6,7 @@ import { useQuery } from '@tanstack/react-query'; import { useModalStore } from '@/shared/stores/useModalStore'; import { useLanguage } from '@/shared/contexts/LanguageContext'; -import { todoQueries } from '@/shared/lib/query/queryKeys'; +import { todoQueries } from '@/shared/lib/query/queryFunction'; import Button from '@/shared/components/Button'; import FormField from '@/shared/components/FormField'; import Dropdown from '@/shared/components/Dropdown'; @@ -29,10 +29,12 @@ export default function NoteCreateModal({ goalId, onConfirm }: NoteCreateModalPr })); return ( -
+
-

{t.note.todoSelectTitle}

+

+ {t.note.todoSelectTitle} +

@@ -55,7 +57,7 @@ export default function NoteCreateModal({ goalId, onConfirm }: NoteCreateModalPr
); } diff --git a/src/features/todo/components/manual/TodoFormModal/index.tsx b/src/features/todo/components/manual/TodoFormModal/index.tsx index dc6c328c..7ae4988b 100644 --- a/src/features/todo/components/manual/TodoFormModal/index.tsx +++ b/src/features/todo/components/manual/TodoFormModal/index.tsx @@ -18,7 +18,7 @@ import TagInput from '../shared/TagInput'; import { ApiError, PatchTodoRequest, PostTodoRequest } from '@/shared/lib/api'; import { usePatchTodo, usePostTodo } from '@/shared/lib/query/mutations'; -import { goalQueries, todoQueries } from '@/shared/lib/query/queryKeys'; +import { goalQueries, todoQueries } from '@/shared/lib/query/queryFunction'; import { useModalStore } from '@/shared/stores/useModalStore'; import { formatDateForAPI } from '@/shared/utils/utils'; import { useLanguage } from '@/shared/contexts/LanguageContext'; @@ -171,14 +171,16 @@ export default function TodoFormModal({ mode, todo, goalDetailId }: TodoFormModa
-

{isEditMode ? t.todo.editTitle : t.todo.createTitle}

+

+ {isEditMode ? t.todo.editTitle : t.todo.createTitle} +

-
diff --git a/src/shared/components/Modal/GithubRepoConnectModal.tsx b/src/shared/components/Modal/GithubRepoConnectModal.tsx index dd0c587f..25c822f9 100644 --- a/src/shared/components/Modal/GithubRepoConnectModal.tsx +++ b/src/shared/components/Modal/GithubRepoConnectModal.tsx @@ -8,7 +8,7 @@ import Button from '@/shared/components/Button'; import { fetchAuth } from '@/shared/lib/api'; import { useConnectGithubRepository } from '@/shared/lib/query/mutations'; -import { githubQueries, goalQueries, userQueries } from '@/shared/lib/query/queryKeys'; +import { githubQueries, goalQueries, userQueries } from '@/shared/lib/query/queryFunction'; import { useModalStore } from '@/shared/stores/useModalStore'; import { GITHUB_DISCONNECTED_SESSION_KEY } from '@/shared/constants/github'; import { GITHUB_AUTH_INTENT_KEY, GITHUB_PROFILE_SNAPSHOT_KEY } from '@/shared/constants/githubAuth'; @@ -41,7 +41,7 @@ export default function GithubRepoConnectModal() { ); return ( -
+
{t.modal.githubRepoConnectTitle} @@ -50,9 +50,7 @@ export default function GithubRepoConnectModal() {
- - {t.modal.githubRepoConnectDesc} - + {t.modal.githubRepoConnectDesc}
{!user?.githubConnected ? ( @@ -62,11 +60,11 @@ export default function GithubRepoConnectModal() { profileImageUrl={user?.profileImageUrl ?? null} /> ) : hasConnectedRepo ? ( -
+
{t.modal.githubRepoAlreadyConnected}
) : availableRepositories.length === 0 ? ( -
+
{t.modal.githubRepoNoAvailable}
) : ( @@ -74,7 +72,7 @@ export default function GithubRepoConnectModal() { {availableRepositories.map((repository) => (

{repository.name}

@@ -153,7 +151,7 @@ function GithubRepoDescription({ {t.modal.cancel}
diff --git a/src/shared/components/Sidebar/NotificationDropdown.tsx b/src/shared/components/Sidebar/NotificationDropdown.tsx index fe0b1d3b..8de14adc 100644 --- a/src/shared/components/Sidebar/NotificationDropdown.tsx +++ b/src/shared/components/Sidebar/NotificationDropdown.tsx @@ -4,7 +4,7 @@ import { useRef, useState, useEffect } from 'react'; import { createPortal } from 'react-dom'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { BellIcon } from 'lucide-react'; -import { notificationQueries } from '@/shared/lib/query/queryKeys'; +import { notificationQueries } from '@/shared/lib/query/queryFunction'; import { fetchNotifications } from '@/shared/lib/api/fetchNotifications'; import { getRelativeTime } from '@/shared/lib/formatters'; import useOnClickOutside from '@/shared/hooks/useOnClickOutside'; @@ -18,7 +18,13 @@ interface NotificationDropdownProps { placement?: 'right' | 'bottom'; } -export default function NotificationDropdown({ isOpen, onOpen, onClose, isSidebarOpen, placement = 'right' }: NotificationDropdownProps) { +export default function NotificationDropdown({ + isOpen, + onOpen, + onClose, + isSidebarOpen, + placement = 'right', +}: NotificationDropdownProps) { const buttonRef = useRef(null); const dropdownRef = useRef(null); const { t } = useLanguage(); @@ -72,7 +78,9 @@ export default function NotificationDropdown({ isOpen, onOpen, onClose, isSideba ref={buttonRef} onClick={() => (isOpen ? onClose() : onOpen())} className={`group hover:text-bearlog-600 relative text-gray-500 ${ - isSidebarOpen ? 'rounded-[999px] border border-gray-200 p-[20px] dark:border-gray-500 dark:bg-gray-850' : 'p-0' + isSidebarOpen + ? 'dark:bg-gray-850 rounded-[999px] border border-gray-200 p-[20px] dark:border-gray-500' + : 'p-0' }`} > @@ -88,7 +96,7 @@ export default function NotificationDropdown({ isOpen, onOpen, onClose, isSideba
{t.notification.title} diff --git a/src/shared/components/Sidebar/SidebarMobileCase/index.tsx b/src/shared/components/Sidebar/SidebarMobileCase/index.tsx index 429d5b51..96bc6401 100644 --- a/src/shared/components/Sidebar/SidebarMobileCase/index.tsx +++ b/src/shared/components/Sidebar/SidebarMobileCase/index.tsx @@ -7,7 +7,7 @@ import PageHeader from '@/shared/components/PageHeader'; import NotificationDropdown from '@/shared/components/Sidebar/NotificationDropdown'; import { useMobileHeaderStore } from '@/shared/stores/useMobileHeaderStore'; import { CurrentUserResponse } from '@/shared/lib/api/fetchUsers'; -import { todoQueries } from '@/shared/lib/query/queryKeys'; +import { todoQueries } from '@/shared/lib/query/queryFunction'; import { useQuery } from '@tanstack/react-query'; import { useLanguage } from '@/shared/contexts/LanguageContext'; diff --git a/src/shared/components/Sidebar/index.tsx b/src/shared/components/Sidebar/index.tsx index 9722d63c..008f492e 100644 --- a/src/shared/components/Sidebar/index.tsx +++ b/src/shared/components/Sidebar/index.tsx @@ -27,7 +27,7 @@ import { useBreakpoint } from '@/shared/hooks/useBreakPoint'; import { useModalStore } from '@/shared/stores/useModalStore'; import { usePostGoal, usePostLogout } from '@/shared/lib/query/mutations'; import { useTodoCreateModal } from '@/features/todo/hooks/useTodoCreateModal'; -import { userQueries } from '@/shared/lib/query/queryKeys'; +import { userQueries } from '@/shared/lib/query/queryFunction'; import { CurrentUserResponse } from '@/shared/lib/api'; import { useLanguage } from '@/shared/contexts/LanguageContext'; import { useTodoModeStore } from '@/shared/stores/useTodoModeStore'; diff --git a/src/shared/contexts/SidebarContext.tsx b/src/shared/contexts/SidebarContext.tsx index 5d6b2592..5bebc84d 100644 --- a/src/shared/contexts/SidebarContext.tsx +++ b/src/shared/contexts/SidebarContext.tsx @@ -5,7 +5,7 @@ import { usePathname } from 'next/navigation'; import { createContext, useContext, useState, ReactNode, useMemo } from 'react'; import { LayoutGridIcon, FlagIcon, ListCheckIcon, StarIcon } from 'lucide-react'; -import { goalQueries } from '@/shared/lib/query/queryKeys'; +import { goalQueries } from '@/shared/lib/query/queryFunction'; import type { GoalListResponse } from '@/shared/lib/api/fetchGoals'; import { useBreakpoint } from '@/shared/hooks/useBreakPoint'; import Image from 'next/image'; diff --git a/src/shared/hooks/useGithubRepoConnectModal.tsx b/src/shared/hooks/useGithubRepoConnectModal.tsx index 8a52709a..2bfa0a5b 100644 --- a/src/shared/hooks/useGithubRepoConnectModal.tsx +++ b/src/shared/hooks/useGithubRepoConnectModal.tsx @@ -5,7 +5,7 @@ import { useQuery } from '@tanstack/react-query'; import GithubRepoConnectModal from '@/shared/components/Modal/GithubRepoConnectModal'; import { GITHUB_DISCONNECTED_SESSION_KEY } from '@/shared/constants/github'; -import { goalQueries } from '@/shared/lib/query/queryKeys'; +import { goalQueries } from '@/shared/lib/query/queryFunction'; import { useModalStore } from '@/shared/stores/useModalStore'; import { useTodoModeStore } from '@/shared/stores/useTodoModeStore'; diff --git a/src/shared/lib/api/fetchDashboard.ts b/src/shared/lib/api/fetchDashboard.ts index 5d06bd10..6cfc6306 100644 --- a/src/shared/lib/api/fetchDashboard.ts +++ b/src/shared/lib/api/fetchDashboard.ts @@ -1,57 +1,21 @@ -import { fetchTodos } from './fetchTodos'; -import { fetchUsers } from './fetchUsers'; import type { DashboardSummaryResponse } from '@/shared/types/api/schemas/api.process'; - -const TODO_RECENT_LIMIT = 4; - -export interface DashboardSummaryErrors { - user?: 'failed'; - progress?: 'failed'; - todos?: 'failed'; -} - -export interface DashboardSummaryResult { - data: DashboardSummaryResponse; - errors: DashboardSummaryErrors; - hasAnySuccess: boolean; +import type { DashboardDetailTodosResponse } from '@/shared/types/api/schemas/api.process'; +import { DashboardSummaryResult } from '../customApi/getDashboardSummaryResult'; +import { getDashboardSummaryResult } from '../customApi/getDashboardSummaryResult'; + +interface DashboardDetailTodosApiResult { + data: { items: DashboardDetailTodosResponse[] }; + errors?: { + goal?: 'failed'; + todos?: 'failed'; + }; + hasAnySuccess?: boolean; } class FetchDashboard { - getDashboardSummaryResult = async (): Promise => { - const [userRes, progressRes, todoRecentRes] = await Promise.allSettled([ - fetchUsers.getCurrentUser(), - fetchUsers.getUserProgress(), - fetchTodos.getTodos({ - sort: 'LATEST', - search: '', - limit: TODO_RECENT_LIMIT, - }), - ]); - - const user = userRes.status === 'fulfilled' ? userRes.value : null; - const progress = progressRes.status === 'fulfilled' ? progressRes.value : null; - const todos = todoRecentRes.status === 'fulfilled' ? todoRecentRes.value : null; - - const errors: DashboardSummaryErrors = { - user: userRes.status === 'rejected' ? 'failed' : undefined, - progress: progressRes.status === 'rejected' ? 'failed' : undefined, - todos: todoRecentRes.status === 'rejected' ? 'failed' : undefined, - }; - - return { - data: { - user: user ? { id: user.id, nickname: user.nickname, githubConnected: user.githubConnected } : null, - progress: progress ? { totalProgress: progress.totalProgress } : null, - todos: todos?.todos ?? [], - }, - errors, - hasAnySuccess: Boolean(user || progress || todos), - }; - }; - getDashboardSummary = async (): Promise => { if (typeof window === 'undefined') { - const { data } = await this.getDashboardSummaryResult(); + const { data } = await getDashboardSummaryResult(); return data; } @@ -71,6 +35,24 @@ class FetchDashboard { const payload = (await response.json()) as DashboardSummaryResult; return payload.data; }; + + getDashboardDetailTodos = async (): Promise<{ items: DashboardDetailTodosResponse[] }> => { + const response = await fetch('/api/dashboard/detail', { + method: 'GET', + headers: { + Accept: 'application/json', + }, + credentials: 'include', + cache: 'no-store', + }); + + if (!response.ok) { + throw new Error(`Dashboard detail todos request failed: ${response.status}`); + } + + const payload = (await response.json()) as DashboardDetailTodosApiResult; + return payload.data; + }; } const fetchDashboard = new FetchDashboard(); diff --git a/src/shared/lib/customApi/getDashboardDetailTodos.ts b/src/shared/lib/customApi/getDashboardDetailTodos.ts new file mode 100644 index 00000000..23e89007 --- /dev/null +++ b/src/shared/lib/customApi/getDashboardDetailTodos.ts @@ -0,0 +1,55 @@ +import { DashboardDetailTodosResponse } from '@/shared/types/api/schemas/api.process'; +import { fetchGoals, fetchTodos } from '../api'; + +export interface DashboardDetailTodosErrors { + goal?: 'failed'; + todos?: 'failed'; +} + +export interface DashboardDetailTodosResult { + data: { items: DashboardDetailTodosResponse[] }; + errors: DashboardDetailTodosErrors; + hasAnySuccess: boolean; +} + +const SAFE_LIMIT = 10; + +const toProgressPercent = (completedCount: number, todoCount: number): number => { + if (todoCount <= 0) return 0; + return Math.round((completedCount / todoCount) * 100); +}; + +export const getDashboardDetailTodos = async (): Promise => { + 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 }), + ]); + + const goals = goalsRes.status === 'fulfilled' ? goalsRes.value.goals : []; + const openTodos = openTodosRes.status === 'fulfilled' ? openTodosRes.value.todos : []; + const doneTodos = doneTodosRes.status === 'fulfilled' ? doneTodosRes.value.todos : []; + + const items: DashboardDetailTodosResponse[] = goals.map((goal) => { + return { + goal: { + id: goal.id, + title: goal.title, + source: goal.source, + progress: toProgressPercent(goal.completedCount, goal.todoCount), + }, + openTodos: openTodos.filter((todo) => todo.goal.id === goal.id).slice(0, SAFE_LIMIT), + doneTodos: doneTodos.filter((todo) => todo.goal.id === goal.id).slice(0, SAFE_LIMIT), + }; + }); + + return { + data: { items }, + errors: { + goal: goalsRes.status === 'rejected' ? 'failed' : undefined, + todos: openTodosRes.status === 'rejected' && doneTodosRes.status === 'rejected' ? 'failed' : undefined, + }, + hasAnySuccess: + goalsRes.status === 'fulfilled' || openTodosRes.status === 'fulfilled' || doneTodosRes.status === 'fulfilled', + }; +}; diff --git a/src/shared/lib/customApi/getDashboardSummaryResult.ts b/src/shared/lib/customApi/getDashboardSummaryResult.ts new file mode 100644 index 00000000..f59ffce5 --- /dev/null +++ b/src/shared/lib/customApi/getDashboardSummaryResult.ts @@ -0,0 +1,48 @@ +import { DashboardSummaryResponse } from '@/shared/types/api/schemas/api.process'; +import { fetchTodos, fetchUsers } from '../api'; + +const TODO_RECENT_LIMIT = 4; + +export interface DashboardSummaryErrors { + user?: 'failed'; + progress?: 'failed'; + todos?: 'failed'; +} + +export interface DashboardSummaryResult { + data: DashboardSummaryResponse; + errors: DashboardSummaryErrors; + hasAnySuccess: boolean; +} + +export const getDashboardSummaryResult = async (): Promise => { + const [userRes, progressRes, todoRecentRes] = await Promise.allSettled([ + fetchUsers.getCurrentUser(), + fetchUsers.getUserProgress(), + fetchTodos.getTodos({ + sort: 'LATEST', + search: '', + limit: TODO_RECENT_LIMIT, + }), + ]); + + const user = userRes.status === 'fulfilled' ? userRes.value : null; + const progress = progressRes.status === 'fulfilled' ? progressRes.value : null; + const todos = todoRecentRes.status === 'fulfilled' ? todoRecentRes.value : null; + + const errors: DashboardSummaryErrors = { + user: userRes.status === 'rejected' ? 'failed' : undefined, + progress: progressRes.status === 'rejected' ? 'failed' : undefined, + todos: todoRecentRes.status === 'rejected' ? 'failed' : undefined, + }; + + return { + data: { + user: user ? { id: user.id, nickname: user.nickname, githubConnected: user.githubConnected } : null, + progress: progress ? { totalProgress: progress.totalProgress } : null, + todos: todos?.todos ?? [], + }, + errors, + hasAnySuccess: Boolean(user || progress || todos), + }; +}; diff --git a/src/shared/lib/query/keyFactory.ts b/src/shared/lib/query/keyFactory.ts index 88cfbff7..2c19a9c0 100644 --- a/src/shared/lib/query/keyFactory.ts +++ b/src/shared/lib/query/keyFactory.ts @@ -58,4 +58,5 @@ export const tagKeys = { export const dashboardKeys = { all: ['dashboard'] as const, summary: () => [...dashboardKeys.all, 'summary'] as const, + detailTodos: () => [...dashboardKeys.all, 'detailTodos'] as const, }; diff --git a/src/shared/lib/query/mutations.ts b/src/shared/lib/query/mutations.ts index 31524cb2..1e13e101 100644 --- a/src/shared/lib/query/mutations.ts +++ b/src/shared/lib/query/mutations.ts @@ -21,7 +21,7 @@ import { PatchCurrentUserRequest, } from '../api/fetchUsers'; import { dashboardKeys, githubKeys, goalKeys, noteKeys, todoKeys, userKeys } from './keyFactory'; -import { noteQueries } from './queryKeys'; +import { noteQueries } from './queryFunction'; import { useToastStore } from '@/shared/stores/useToastStore'; import { useLanguage } from '@/shared/contexts/LanguageContext'; import { useTodoModeStore } from '@/shared/stores/useTodoModeStore'; @@ -63,6 +63,7 @@ export const usePostGoal = () => { showToast(t.mutations.goalCreated); queryClient.invalidateQueries({ queryKey: goalKeys.lists() }); queryClient.invalidateQueries({ queryKey: dashboardKeys.summary() }); + queryClient.invalidateQueries({ queryKey: dashboardKeys.detailTodos() }); }, }); }; @@ -95,6 +96,7 @@ export const useDeleteGoal = (goalId?: number) => { showToast(t.mutations.goalDeleted); queryClient.invalidateQueries({ queryKey: goalKeys.lists() }); queryClient.invalidateQueries({ queryKey: dashboardKeys.summary() }); + queryClient.invalidateQueries({ queryKey: dashboardKeys.detailTodos() }); router.push('/dashboard'); }, onError: () => { @@ -139,6 +141,7 @@ export const usePatchGoal = (goalId?: number) => { queryClient.invalidateQueries({ queryKey: goalKeys.lists() }); queryClient.invalidateQueries({ queryKey: dashboardKeys.summary() }); + queryClient.invalidateQueries({ queryKey: dashboardKeys.detailTodos() }); }, onError: () => { showToast(t.mutations.goalUpdateFail, 'fail'); @@ -162,6 +165,7 @@ export const useConnectGithubRepository = () => { queryClient.invalidateQueries({ queryKey: githubKeys.repositories() }); queryClient.invalidateQueries({ queryKey: userKeys.progress() }); queryClient.invalidateQueries({ queryKey: dashboardKeys.summary() }); + queryClient.invalidateQueries({ queryKey: dashboardKeys.detailTodos() }); }, onError: () => { showToast(t.mutations.githubRepositoryConnectFail, 'fail'); @@ -204,6 +208,7 @@ export const useDisconnectGithubGoal = (goalId?: number) => { queryClient.invalidateQueries({ queryKey: githubKeys.repositories() }); queryClient.invalidateQueries({ queryKey: userKeys.progress() }); queryClient.invalidateQueries({ queryKey: dashboardKeys.summary() }); + queryClient.invalidateQueries({ queryKey: dashboardKeys.detailTodos() }); router.push('/dashboard'); }, onError: (_error, _variables, context) => { @@ -257,6 +262,7 @@ export const usePostTodo = () => { queryClient.invalidateQueries({ queryKey: goalKeys.details() }); queryClient.invalidateQueries({ queryKey: userKeys.progress() }); queryClient.invalidateQueries({ queryKey: dashboardKeys.summary() }); + queryClient.invalidateQueries({ queryKey: dashboardKeys.detailTodos() }); }, onError: (error) => { console.error(error); @@ -295,6 +301,7 @@ export const useDeleteTodo = (todoId?: number) => { queryClient.invalidateQueries({ queryKey: goalKeys.details() }); queryClient.invalidateQueries({ queryKey: userKeys.progress() }); queryClient.invalidateQueries({ queryKey: dashboardKeys.summary() }); + queryClient.invalidateQueries({ queryKey: dashboardKeys.detailTodos() }); }, onError: () => { showToast(t.mutations.todoDeleteFail, 'fail'); @@ -430,6 +437,7 @@ export const usePatchTodo = (todoId?: number) => { queryClient.invalidateQueries({ queryKey: goalKeys.details() }); queryClient.invalidateQueries({ queryKey: userKeys.progress() }); queryClient.invalidateQueries({ queryKey: dashboardKeys.summary() }); + queryClient.invalidateQueries({ queryKey: dashboardKeys.detailTodos() }); }, onError: (_error, _variables, context) => { @@ -492,6 +500,7 @@ export const usePatchTodoFavorite = (todoId?: number) => { queryClient.invalidateQueries({ queryKey: goalKeys.details() }); queryClient.invalidateQueries({ queryKey: userKeys.progress() }); queryClient.invalidateQueries({ queryKey: dashboardKeys.summary() }); + queryClient.invalidateQueries({ queryKey: dashboardKeys.detailTodos() }); }, onError: (_error, _variables, context) => { if (todoId !== undefined && context?.previousTodo !== undefined) { @@ -513,6 +522,7 @@ export const usePatchCurrentUser = () => { onSuccess: () => { queryClient.invalidateQueries({ queryKey: userKeys.me() }); queryClient.invalidateQueries({ queryKey: dashboardKeys.summary() }); + queryClient.invalidateQueries({ queryKey: dashboardKeys.detailTodos() }); }, onError: () => { showToast(t.mutations.userUpdateFail, 'fail'); @@ -572,6 +582,7 @@ export const useDeleteGithubConnection = () => { queryClient.invalidateQueries({ queryKey: githubKeys.repositories() }); queryClient.invalidateQueries({ queryKey: userKeys.progress() }); queryClient.invalidateQueries({ queryKey: dashboardKeys.summary() }); + queryClient.invalidateQueries({ queryKey: dashboardKeys.detailTodos() }); router.push('/dashboard'); }, onError: () => { @@ -643,6 +654,7 @@ export const usePatchNote = (noteId: number, goalId: number) => { onError: () => showToast(t.mutations.noteUpdateFail, 'fail'), }); }; + export const useDeleteNote = (noteId: number, goalId: number) => { const router = useRouter(); const searchParams = useSearchParams(); diff --git a/src/shared/lib/query/queryKeys.ts b/src/shared/lib/query/queryFunction.ts similarity index 95% rename from src/shared/lib/query/queryKeys.ts rename to src/shared/lib/query/queryFunction.ts index 1f1d5aa8..20aa8e38 100644 --- a/src/shared/lib/query/queryKeys.ts +++ b/src/shared/lib/query/queryFunction.ts @@ -170,4 +170,11 @@ export const dashboardQueries = { queryFn: () => fetchDashboard.getDashboardSummary(), staleTime: DASHBOARD_STALE_TIME, }), + + detailTodos: () => + queryOptions({ + queryKey: dashboardKeys.detailTodos(), + queryFn: () => fetchDashboard.getDashboardDetailTodos(), + staleTime: DASHBOARD_STALE_TIME, + }), }; diff --git a/src/shared/types/api/schemas/api.process.ts b/src/shared/types/api/schemas/api.process.ts index dd5700c9..db675717 100644 --- a/src/shared/types/api/schemas/api.process.ts +++ b/src/shared/types/api/schemas/api.process.ts @@ -1,4 +1,4 @@ -import { CurrentUserResponse, TodoListResponse, UserProgressResponse } from '@/shared/lib/api'; +import { CurrentUserResponse, GoalDetailResponse, TodoListResponse, UserProgressResponse } from '@/shared/lib/api'; type DashboardUser = Pick; type DashboardProgress = Pick; @@ -9,3 +9,12 @@ export interface DashboardSummaryResponse { progress: DashboardProgress | null; todos: DashboardTodo[]; } + +type DashboardDetailInGoalDetail = Pick; +type DashboardDetailInTodoDetail = TodoListResponse['todos'][number]; + +export interface DashboardDetailTodosResponse { + goal: DashboardDetailInGoalDetail; + openTodos: DashboardDetailInTodoDetail[]; + doneTodos: DashboardDetailInTodoDetail[]; +}