Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export const useHomeTodosByDate = (
const { mutate: stopTimer } = useStopTimer();
const {
invalidateHomeView,
invalidateStatistics,
invalidateTimerState,
invalidateTimeBoxes,
invalidateFocusTodo,
Expand Down Expand Up @@ -120,6 +121,7 @@ export const useHomeTodosByDate = (
invalidateHomeAndFocus();
invalidateTimeBoxes();
invalidateTodoDetail(dateKey, todoId);
invalidateStatistics();
},
onError: () => {
setTodosByDate((prev) => ({ ...prev, [dateKey]: previous }));
Expand Down Expand Up @@ -148,6 +150,7 @@ export const useHomeTodosByDate = (
onSuccess: () => {
invalidateHomeAndFocus();
invalidateTodoDetail(dateKey, todoId);
invalidateStatistics();
},
},
);
Expand Down Expand Up @@ -216,6 +219,7 @@ export const useHomeTodosByDate = (
invalidateHomeAndFocus();
invalidateTimeBoxes();
invalidateTodoDetail(dateKey, todoId);
invalidateStatistics();
},
onError: () => {
setTodosByDate((prev) => ({ ...prev, [dateKey]: previous }));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export const StatisticsCalendar = ({
return (
<section className="min-w-0 flex-1 overflow-x-auto px-14.75 pt-10 pb-13">
<div className="w-199.5">
<div className="sticky top-0 z-10 bg-white pb-5">
<div className="pb-5">
<div className="flex flex-col gap-2 pb-[69px]">
<h1 className="typo-headline-b-30 text-timo-gray-900">
{formatStatisticsMonth(currentMonth, locale)}
Expand All @@ -112,7 +112,7 @@ export const StatisticsCalendar = ({
</div>
</div>

<div className="grid grid-cols-7 gap-x-14 gap-y-5 bg-white">
<div className="grid grid-cols-7 gap-x-14 gap-y-5">
{Array.from({ length: firstDayOffset }, (_, index) => (
<div key={`empty-${index}`} />
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export type StatisticsSidePanelProps =
| StatisticsDaySidePanelProps;

const SIDE_PANEL_CLASS_NAME =
"border-timo-gray-500 bg-white h-full w-[304px] shrink-0 border-l text-timo-black";
"border-timo-gray-500 bg-white h-full w-[304px] shrink-0 overflow-y-auto border-l text-timo-black";

const getDiffLabel = (
diffMinutes: number,
Expand Down Expand Up @@ -102,14 +102,18 @@ export const StatisticsSidePanel = (props: StatisticsSidePanelProps) => {
</div>

<ul className="mt-5.5 flex flex-col">
{detail.todos.map((todo) => {
{detail.todos.map((todo, index) => {
const diffMinutes =
todo.actualTimeMinutes - todo.estimatedTimeMinutes;
const isLastTodo = index === detail.todos.length - 1;

return (
<li
key={todo.todoId}
className="border-timo-gray-500 flex justify-between border-b py-2.5"
className={cn(
"flex justify-between py-2.5 pt-5",
!isLastTodo && "border-timo-gray-500 border-b",
)}
Comment on lines +105 to +116

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Tailwind의 last: 모디파이어를 활용해 보세요.

JS에서 배열 인덱스를 계산하여 조건부로 스타일을 적용하는 대신, Tailwind CSS의 의사 클래스(pseudo-class) 모디파이어인 last:를 사용하면 코드를 한결 간결하게 작성할 수 있습니다. 렌더링 성능과 가독성 측면에서도 CSS 기반 처리가 권장됩니다! ✨

자세한 내용은 Tailwind CSS 공식 문서: Pseudo-classes를 참고해 보세요.

💡 제안하는 리팩토링 코드
-        {detail.todos.map((todo, index) => {
+        {detail.todos.map((todo) => {
           const diffMinutes =
             todo.actualTimeMinutes - todo.estimatedTimeMinutes;
-          const isLastTodo = index === detail.todos.length - 1;
 
           return (
             <li
               key={todo.todoId}
-              className={cn(
-                "flex justify-between py-2.5 pt-5",
-                !isLastTodo && "border-timo-gray-500 border-b",
-              )}
+              className="border-timo-gray-500 flex justify-between border-b py-2.5 pt-5 last:border-b-0"
             >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{detail.todos.map((todo, index) => {
const diffMinutes =
todo.actualTimeMinutes - todo.estimatedTimeMinutes;
const isLastTodo = index === detail.todos.length - 1;
return (
<li
key={todo.todoId}
className="border-timo-gray-500 flex justify-between border-b py-2.5"
className={cn(
"flex justify-between py-2.5 pt-5",
!isLastTodo && "border-timo-gray-500 border-b",
)}
{detail.todos.map((todo) => {
const diffMinutes =
todo.actualTimeMinutes - todo.estimatedTimeMinutes;
return (
<li
key={todo.todoId}
className="border-timo-gray-500 flex justify-between border-b py-2.5 pt-5 last:border-b-0"
>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/timo-web/app/`[locale]/(main)/statistics/_components/StatisticsSidePanel.tsx
around lines 105 - 116, In the todo list rendering around detail.todos.map,
remove the isLastTodo index calculation and apply the border styling with
Tailwind’s last: modifier on each li instead of conditionally checking the array
position. Preserve the existing spacing and ensure the last item does not
receive the bottom border.

>
<div className="flex flex-col items-start gap-2">
<p className="typo-headline-r-14 text-timo-gray-900">
Expand All @@ -128,7 +132,9 @@ export const StatisticsSidePanel = (props: StatisticsSidePanelProps) => {
{formatStatisticsClockText(todo.estimatedTimeMinutes)}
</span>

<span className={getDiffClassName(diffMinutes)}>
<span
className={cn("min-h-[18px]", getDiffClassName(diffMinutes))}
>
{getDiffLabel(diffMinutes, t)}
</span>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
"use client";

import { useTranslations } from "next-intl";

import { StatisticsCalendar } from "@/app/[locale]/(main)/statistics/_components/StatisticsCalendar";
import { useStatisticsCalendarQuery } from "@/app/[locale]/(main)/statistics/_queries/use-statistics-query";
import { LottiePlayer } from "@/components/lottie/LottiePlayer";
import { formatDateKey } from "@/utils/date/date";

interface StatisticsCalendarContainerProps {
Expand All @@ -14,56 +11,18 @@ interface StatisticsCalendarContainerProps {
onSelectDate: (date: Date) => void;
}

interface StatisticsCalendarStatusProps {
lottieSrc: string;
message: string;
role?: "status" | "alert";
}

const StatisticsCalendarStatus = ({
lottieSrc,
message,
role = "status",
}: StatisticsCalendarStatusProps) => {
return (
<section
className="flex min-w-0 flex-1 flex-col items-center justify-center gap-6 px-14.75 pt-10 pb-13 text-center"
role={role}
>
<LottiePlayer src={lottieSrc} className="w-[143px]" ariaLabel={message} />
<p className="typo-headline-m-14 text-timo-gray-900">{message}</p>
</section>
);
};

export const StatisticsCalendarContainer = ({
currentMonth,
displayDate,
selectedDate,
onSelectDate,
}: StatisticsCalendarContainerProps) => {
const t = useTranslations("Statistics.calendar");
const yearMonth = formatDateKey(currentMonth).slice(0, 7);
const calendarQuery = useStatisticsCalendarQuery(yearMonth);
const calendarData = calendarQuery.data?.data;

if (calendarQuery.isPending) {
return (
<StatisticsCalendarStatus
lottieSrc="/lottie/loading.json"
message={t("loading")}
/>
);
}

if (calendarQuery.isError || !calendarData) {
return (
<StatisticsCalendarStatus
Comment thread
yumin-kim2 marked this conversation as resolved.
lottieSrc="/lottie/error.json"
message={t("error")}
role="alert"
/>
);
if (calendarQuery.isPending || calendarQuery.isError || !calendarData) {
return null;
}

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,13 @@ export const StatisticsContainer = () => {
};

return (
<div className="flex h-full">
<div className="flex min-w-0 flex-1 flex-col">
<div className="flex h-full overflow-hidden">
<div className="flex min-w-0 flex-1 flex-col overflow-hidden">
<StatisticsHeaderContainer
currentMonth={currentMonth}
onChangeMonth={handleChangeMonth}
/>
<div className="flex min-h-0 flex-1">
<div className="min-h-0 flex-1 overflow-auto">
<StatisticsCalendarContainer
currentMonth={currentMonth}
displayDate={selectedDate}
Expand Down
2 changes: 1 addition & 1 deletion apps/timo-web/components/lottie/LottiePlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const LottiePlayer = ({
}, [src]);

return (
<div className={cn("w-full", className)}>
<div className={cn(className)}>
{animationData && (
<Lottie
animationData={animationData}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"use client";

import { useQueryClient } from "@tanstack/react-query";

import {
getGetCalendarQueryKey,
getGetDailyQueryKey,
getGetSummaryQueryKey,
} from "@/api/generated/endpoints/statistics/statistics";

export const useStatisticsQueryInvalidation = () => {
const queryClient = useQueryClient();

const invalidateStatistics = () => {
queryClient.invalidateQueries({ queryKey: getGetSummaryQueryKey() });
queryClient.invalidateQueries({ queryKey: getGetDailyQueryKey() });
queryClient.invalidateQueries({ queryKey: getGetCalendarQueryKey() });
};
Comment on lines +14 to +18

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Promise.all을 활용해 비동기 무효화 작업의 Promise를 반환해 보세요.

현재 invalidateQueries는 실행 후 무효화 처리에 대한 Promise를 반환하지만, 여러 무효화 함수를 묶어서 호출하는 래퍼 함수들(invalidateStatistics, invalidateTimerState)이 해당 결과 Promise를 반환(return)하지 않고 있습니다.

이렇게 되면 호출부(예: Mutation의 onSuccess 콜백)에서 쿼리 무효화 완료 시점을 명시적으로 기다려야 할 때 후속 처리가 불가능해집니다. 각 호출을 Promise.all 배열로 묶어 반환하면 보다 유연하고 안정적인 사용자 경험(UX) 제어가 가능해집니다. 멋진 통계 기능이 더욱 견고하게 동작할 수 있을 거예요! ✨

  • apps/timo-web/hooks/statistics/use-statistics-query-invalidation.ts#L14-L18: 내부의 queryClient.invalidateQueries 호출들을 Promise.all([ ... ])로 감싸서 반환하도록 수정해 보세요.
  • apps/timo-web/hooks/timer/use-timer-query-invalidation.ts#L20-L25: 내부의 무효화 함수 호출들(invalidateActiveTimer(), invalidateHomeView() 등)을 Promise.all([ ... ])로 감싸서 반환하도록 수정해 보세요.

관련하여 TanStack Query 공식 문서 - Query Invalidation 가이드를 확인해 보시면 더욱 도움이 될 것입니다.

📍 Affects 2 files
  • apps/timo-web/hooks/statistics/use-statistics-query-invalidation.ts#L14-L18 (this comment)
  • apps/timo-web/hooks/timer/use-timer-query-invalidation.ts#L20-L25
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/timo-web/hooks/statistics/use-statistics-query-invalidation.ts` around
lines 14 - 18, Update invalidateStatistics in
apps/timo-web/hooks/statistics/use-statistics-query-invalidation.ts lines 14-18
to return a Promise.all containing all queryClient.invalidateQueries calls. Also
update invalidateTimerState in
apps/timo-web/hooks/timer/use-timer-query-invalidation.ts lines 20-25 to return
a Promise.all containing invalidateActiveTimer(), invalidateHomeView(), and the
other existing invalidation calls, preserving their current behavior while
exposing completion to callers.

Comment thread
yumin-kim2 marked this conversation as resolved.

return {
invalidateStatistics,
};
};
4 changes: 4 additions & 0 deletions apps/timo-web/hooks/timer/use-timer-query-invalidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import {
} from "@/api/generated/endpoints/home/home";
import { getGetTimeBoxesQueryKey } from "@/api/generated/endpoints/time-box/time-box";
import { getGetActiveTimerQueryKey } from "@/api/generated/endpoints/timer/timer";
import { useStatisticsQueryInvalidation } from "@/hooks/statistics/use-statistics-query-invalidation";

export const useTimerQueryInvalidation = () => {
const queryClient = useQueryClient();
const { invalidateStatistics } = useStatisticsQueryInvalidation();

const invalidateActiveTimer = () =>
queryClient.invalidateQueries({ queryKey: getGetActiveTimerQueryKey() });
Expand All @@ -27,11 +29,13 @@ export const useTimerQueryInvalidation = () => {
invalidateActiveTimer();
invalidateHomeView();
invalidateTimeBoxes();
invalidateStatistics();
};

return {
invalidateActiveTimer,
invalidateHomeView,
invalidateStatistics,
invalidateTimeBoxes,
invalidateTodayView,
invalidateFocusTodo,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { CreateTodoRequest } from "@/schemas/todo/todo-schema";

import { getGetHomeQueryKey } from "@/api/generated/endpoints/home/home";
import { useCreateTodo } from "@/api/generated/endpoints/todo/todo";
import { useStatisticsQueryInvalidation } from "@/hooks/statistics/use-statistics-query-invalidation";
import { todoCreateResponseSchema } from "@/schemas/todo/todo-schema";

const buildCreateTodoRequestBody = (
Expand All @@ -30,6 +31,7 @@ export const useCreateTodoSubmit = () => {
const [isErrorToastOpen, setIsErrorToastOpen] = useState(false);
const { mutate: createTodo } = useCreateTodo();
const queryClient = useQueryClient();
const { invalidateStatistics } = useStatisticsQueryInvalidation();

const handleSubmit = (data: CreateTodoRequest) => {
createTodo(
Expand All @@ -44,6 +46,7 @@ export const useCreateTodoSubmit = () => {
}

queryClient.invalidateQueries({ queryKey: getGetHomeQueryKey() });
invalidateStatistics();
},
onError: () => {
setIsErrorToastOpen(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
getGetTodayQueryKey,
} from "@/api/generated/endpoints/home/home";
import { useDeleteTodo } from "@/api/generated/endpoints/todo/todo";
import { useStatisticsQueryInvalidation } from "@/hooks/statistics/use-statistics-query-invalidation";

export interface DeleteTodoSubmitHandlers {
onSuccess: () => void;
Expand All @@ -20,6 +21,7 @@ export interface DeleteTodoSubmitHandlers {
export const useDeleteTodoSubmit = () => {
const { mutate: deleteTodo } = useDeleteTodo();
const queryClient = useQueryClient();
const { invalidateStatistics } = useStatisticsQueryInvalidation();

const handleDelete = (
todoId: number,
Expand All @@ -31,6 +33,7 @@ export const useDeleteTodoSubmit = () => {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: getGetHomeQueryKey() });
queryClient.invalidateQueries({ queryKey: getGetTodayQueryKey() });
invalidateStatistics();
queryClient.invalidateQueries({
queryKey: getGetFocusTodoQueryKey(),
});
Comment on lines 37 to 39

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

공통 쿼리 무효화 훅(Hook)으로의 분리 고려

현재 여러 컴포넌트나 훅에서 queryClient.invalidateQueries를 직접 호출하여 홈, 오늘, 포커스 화면 등의 쿼리를 개별적으로 무효화하고 있습니다. React Query가 연속된 무효화 호출을 자동으로 배치 처리해주기 때문에 기능이나 성능상의 문제는 전혀 없습니다!

다만, 향후 유지보수와 코드 일관성을 위해 중복되는 무효화 로직을 useTodoQueryInvalidation과 같은 하나의 공통 훅으로 묶어서 관리하는 것을 추천합니다.

  • apps/timo-web/hooks/todo-modal/detail/use-delete-todo-submit.ts#L37-L39: 삭제 성공 시 호출되는 쿼리 무효화 로직을 공통 훅으로 추상화하는 것을 고려해 보세요.
  • apps/timo-web/hooks/todo-modal/detail/use-update-todo-submit.ts#L49-L51: 수정 성공 시 호출되는 무효화 로직 역시 동일한 방식으로 묶어낼 수 있습니다.

깔끔하게 필요한 무효화 로직을 잘 추가해주셨네요! 👍

참고 문서: React Query Query Invalidation

📍 Affects 2 files
  • apps/timo-web/hooks/todo-modal/detail/use-delete-todo-submit.ts#L37-L39 (this comment)
  • apps/timo-web/hooks/todo-modal/detail/use-update-todo-submit.ts#L49-L51
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/timo-web/hooks/todo-modal/detail/use-delete-todo-submit.ts` around lines
37 - 39, Extract the duplicated todo query invalidation logic into a shared hook
such as useTodoQueryInvalidation, then reuse it for the deletion success path in
apps/timo-web/hooks/todo-modal/detail/use-delete-todo-submit.ts (lines 37-39)
and the update success path in
apps/timo-web/hooks/todo-modal/detail/use-update-todo-submit.ts (lines 49-51).
Preserve all currently invalidated query keys and invoke the shared hook from
both submit hooks.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
getGetTodoDetailQueryKey,
useUpdateTodo,
} from "@/api/generated/endpoints/todo/todo";
import { useStatisticsQueryInvalidation } from "@/hooks/statistics/use-statistics-query-invalidation";

export interface UpdateTodoSubmitParams {
todoId: number;
Expand All @@ -29,6 +30,7 @@ export interface UpdateTodoSubmitHandlers {
export const useUpdateTodoSubmit = () => {
const { mutate: updateTodo } = useUpdateTodo();
const queryClient = useQueryClient();
const { invalidateStatistics } = useStatisticsQueryInvalidation();

const handleUpdate = (
{ todoId, date, data }: UpdateTodoSubmitParams,
Expand All @@ -43,6 +45,7 @@ export const useUpdateTodoSubmit = () => {
queryClient.invalidateQueries({
queryKey: getGetTodoDetailQueryKey(todoId, { date }),
});
invalidateStatistics();
queryClient.invalidateQueries({
queryKey: getGetFocusTodoQueryKey(),
});
Expand Down
Loading