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
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import type { ApiError } from "@/api/error/api-error";
import type { HomeViewDay } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_types/home-view-type";
import type { Todo } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_types/todo-type";

import { getGetFocusTodoQueryKey } from "@/api/generated/endpoints/focus/focus";
import {
useChangeStatus,
useStartTimer,
Expand Down Expand Up @@ -48,14 +47,28 @@ export const useHomeTodosByDate = (
const { mutate: changeSubtaskStatus } = useChangeSubtaskStatus();
const { mutate: reorderTodo } = useReorderTodo();
const { mutate: stopTimer } = useStopTimer();
const { invalidateHomeView, invalidateTimerState, invalidateTimeBoxes } =
useTimerQueryInvalidation();
const {
invalidateHomeView,
invalidateTimerState,
invalidateTimeBoxes,
invalidateFocusTodo,
} = useTimerQueryInvalidation();

const { mutate: startTimer } = useStartTimer<ApiError>({
mutation: { onSuccess: invalidateTimerState },
mutation: {
onSuccess: () => {
invalidateTimerState();
invalidateFocusTodo();
},
},
});
const { mutate: changeStatus } = useChangeStatus({
mutation: { onSuccess: invalidateTimerState },
mutation: {
onSuccess: () => {
invalidateTimerState();
invalidateFocusTodo();
},
},
});

useEffect(() => {
Expand All @@ -77,9 +90,6 @@ export const useHomeTodosByDate = (
}));
};

const invalidateFocusTodo = () => {
queryClient.invalidateQueries({ queryKey: getGetFocusTodoQueryKey() });
};
const invalidateHomeAndFocus = () => {
invalidateHomeView();
invalidateFocusTodo();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import type { ErrorType } from "@/api/client/custom-instance";
import type { ErrorDto } from "@/api/generated/models";
import type { TodayTodo } from "@/app/[locale]/(main)/(with-time-sidebar)/today/_types/today-type";

import { getGetTodayQueryKey } from "@/api/generated/endpoints/home/home";
import {
useChangeStatus,
useStartTimer,
Expand Down Expand Up @@ -45,14 +44,28 @@ export const useTodayTodoList = (
const { mutate: changeTodoStatus } = useChangeTodoStatus();
const { mutate: changeSubtaskStatus } = useChangeSubtaskStatus();
const { mutate: stopTimer } = useStopTimer();
const { invalidateTimerState, invalidateTimeBoxes } =
useTimerQueryInvalidation();
const {
invalidateTimerState,
invalidateTimeBoxes,
invalidateTodayView,
invalidateFocusTodo,
} = useTimerQueryInvalidation();

const { mutate: startTimer } = useStartTimer({
mutation: { onSuccess: invalidateTimerState },
mutation: {
onSuccess: () => {
invalidateTimerState();
invalidateFocusTodo();
},
},
});
const { mutate: changeStatus } = useChangeStatus({
mutation: { onSuccess: invalidateTimerState },
mutation: {
onSuccess: () => {
invalidateTimerState();
invalidateFocusTodo();
},
},
});

useEffect(() => {
Expand All @@ -68,9 +81,6 @@ export const useTodayTodoList = (
);
};

const invalidateTodayView = () => {
queryClient.invalidateQueries({ queryKey: getGetTodayQueryKey() });
};
const invalidateTodoDetail = (todoId: number, dateKey: string) => {
queryClient.invalidateQueries({
queryKey: getGetTodoDetailQueryKey(todoId, { date: dateKey }),
Expand All @@ -96,6 +106,7 @@ export const useTodayTodoList = (
invalidateTodayView();
invalidateTimeBoxes();
invalidateTodoDetail(todoId, dateKey);
invalidateFocusTodo();
},
onError: () => {
setTodos(previous);
Expand Down Expand Up @@ -124,6 +135,7 @@ export const useTodayTodoList = (
onSuccess: () => {
invalidateTodayView();
invalidateTodoDetail(todoId, dateKey);
invalidateFocusTodo();
},
},
);
Expand Down Expand Up @@ -207,6 +219,7 @@ export const useTodayTodoList = (
onSuccess: () => {
invalidateTodayView();
invalidateTodoDetail(todoId, dateKey);
invalidateFocusTodo();
},
onError: () => {
setTodos(previous);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export const FocusTaskItem = ({

<div className="flex w-full items-center gap-2.5">
<Checkbox checked={completed} onChange={onToggleCompleted} />
<p className="typo-headline-b-22 text-timo-black min-w-0 flex-1 wrap-break-word">
<p className="typo-headline-b-22 text-timo-black min-w-0 flex-1 wrap-anywhere">
{title}
</p>
</div>
Expand Down Expand Up @@ -82,7 +82,7 @@ export const FocusTaskItem = ({
</PlayButton>
</div>

<div className="border-timo-gray-500 flex min-h-26 w-full min-w-0 flex-col gap-3 border-t pt-3">
<div className="border-timo-gray-500 flex max-h-[40vh] min-h-26 w-full min-w-0 flex-col gap-3 overflow-y-auto border-t pt-3">
{subtasks.map((subtask) => (
<div key={subtask.subtaskId} className="flex items-center gap-2">
<Checkbox
Expand All @@ -93,7 +93,7 @@ export const FocusTaskItem = ({
/>
<p
className={cn(
"typo-body-r-12 min-w-0 flex-1 wrap-break-word",
"typo-body-r-12 min-w-0 flex-1 wrap-anywhere",
subtask.completed ? "text-timo-gray-700" : "text-timo-black",
)}
>
Expand All @@ -103,7 +103,7 @@ export const FocusTaskItem = ({
))}

{memo && (
<p className="typo-body-r-12 text-timo-gray-800 min-w-0 wrap-break-word">
<p className="typo-body-r-12 text-timo-gray-800 min-w-0 wrap-anywhere">
{memo}
</p>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { useEffect, useRef } from "react";

import type { TimerSessionControlsHandle } from "@/components/timer/TimerSessionControls";

import { getGetFocusTodoQueryKey } from "@/api/generated/endpoints/focus/focus";
import {
useChangeStatus,
useCompleteTimer,
Expand All @@ -14,6 +13,7 @@ import {
useStopTimer,
} from "@/api/generated/endpoints/timer/timer";
import {
getGetTodoDetailQueryKey,
useChangeSubtaskStatus,
useChangeTodoStatus,
} from "@/api/generated/endpoints/todo/todo";
Expand Down Expand Up @@ -41,10 +41,20 @@ export const useFocusSession = ({
const { data: focusView } = useFocusTodoQuery();
const { data: activeTimer } = useActiveTimer();

const { invalidateActiveTimer, invalidateHomeView, invalidateTimeBoxes } =
useTimerQueryInvalidation();
const invalidateFocusTodo = () =>
queryClient.invalidateQueries({ queryKey: getGetFocusTodoQueryKey() });
const {
invalidateActiveTimer,
invalidateHomeView,
invalidateTimeBoxes,
invalidateTodayView,
invalidateFocusTodo,
} = useTimerQueryInvalidation();
const invalidateTodoDetail = () => {
const todo = focusView.todo;
if (!todo) return;
queryClient.invalidateQueries({
queryKey: getGetTodoDetailQueryKey(todo.todoId, { date: focusView.date }),
});
};

const { mutate: startTimer } = useStartTimer({
mutation: {
Expand Down Expand Up @@ -84,6 +94,7 @@ export const useFocusSession = ({
invalidateFocusTodo();
invalidateHomeView();
invalidateTimeBoxes();
invalidateTodayView();
},
onError: onMutationError,
},
Expand All @@ -96,6 +107,7 @@ export const useFocusSession = ({
invalidateFocusTodo();
invalidateHomeView();
invalidateTimeBoxes();
invalidateTodayView();
},
onError: onMutationError,
},
Expand All @@ -105,6 +117,8 @@ export const useFocusSession = ({
onSuccess: () => {
invalidateFocusTodo();
invalidateTimeBoxes();
invalidateTodayView();
invalidateTodoDetail();
},
onError: onMutationError,
},
Expand All @@ -114,6 +128,8 @@ export const useFocusSession = ({
onSuccess: () => {
invalidateFocusTodo();
invalidateTimeBoxes();
invalidateTodayView();
invalidateTodoDetail();
},
onError: onMutationError,
},
Expand Down
15 changes: 13 additions & 2 deletions apps/timo-web/components/layout/sidebar/time/TimerPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,13 @@ export const TimerPanel = () => {
const wasTimeUpRef = useRef(false);

const { data: activeTimer } = useActiveTimer();
const { invalidateActiveTimer, invalidateHomeView, invalidateTimeBoxes } =
useTimerQueryInvalidation();
const {
invalidateActiveTimer,
invalidateHomeView,
invalidateTimeBoxes,
invalidateTodayView,
invalidateFocusTodo,
} = useTimerQueryInvalidation();

const { mutate: changeStatus } = useChangeStatus({
mutation: {
Expand All @@ -56,6 +61,8 @@ export const TimerPanel = () => {
invalidateActiveTimer();
invalidateHomeView();
invalidateTimeBoxes();
invalidateTodayView();
invalidateFocusTodo();
},
},
});
Expand All @@ -66,6 +73,8 @@ export const TimerPanel = () => {
invalidateActiveTimer();
invalidateHomeView();
invalidateTimeBoxes();
invalidateTodayView();
invalidateFocusTodo();
},
},
});
Expand All @@ -74,6 +83,8 @@ export const TimerPanel = () => {
onSuccess: () => {
invalidateHomeView();
invalidateTimeBoxes();
invalidateTodayView();
invalidateFocusTodo();
},
},
});
Expand Down
24 changes: 21 additions & 3 deletions apps/timo-web/components/modal/OverlayModal.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
"use client";

import { cn, hasOpenFloatingLayer } from "@repo/timo-design-system/utils";
import {
acquireModalStackIndex,
cn,
hasOpenFloatingLayer,
} from "@repo/timo-design-system/utils";
import FocusTrap from "focus-trap-react";
import { useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";

import type { ReactNode } from "react";

const EXIT_ANIMATION_DURATION = 200;
// tailwind-config theme.css의 --z-index-modal-overlay/--z-index-modal-panel과 동일한 기준값이다.
const BASE_OVERLAY_Z_INDEX = 40;
const BASE_PANEL_Z_INDEX = 50;
// 중첩된 모달의 오버레이가 그 아래 모달의 패널(z=50)보다 항상 높도록, 한 단계당 overlay/panel 차이(10)보다 큰 폭으로 올린다.
const MODAL_STACK_Z_INDEX_STEP = 20;

interface OverlayModalProps {
isOpen: boolean;
Expand All @@ -28,6 +37,7 @@ export const OverlayModal = ({
}: OverlayModalProps) => {
const [shouldRender, setShouldRender] = useState(isOpen);
const [isVisible, setIsVisible] = useState(false);
const [stackIndex, setStackIndex] = useState(0);
const dialogRef = useRef<HTMLDivElement>(null);
const wasFloatingLayerOpenRef = useRef(false);

Expand All @@ -43,6 +53,7 @@ export const OverlayModal = ({
return () => clearTimeout(hideTimer);
}

setStackIndex(acquireModalStackIndex());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

useEffect 의존성 배열로 인한 불필요한 스택 인덱스 증가를 방지하세요.

현재 의존성 배열에 onExited가 포함되어 있어, 부모 컴포넌트에서 onExited 콜백의 참조가 바뀔 때마다 useEffect가 재실행될 위험이 있습니다. 모달이 이미 열려있는 상태(isOpen === true)임에도 불구하고 acquireModalStackIndex()가 반복 호출되면, 전역 스택 인덱스가 계속 증가하여 z-index 체계가 꼬이거나 불필요한 리렌더링이 발생할 수 있습니다.

리렌더링 시에도 안전하게 최신 콜백을 참조할 수 있도록 onExiteduseRef로 관리하거나, 의존성 배열에서 안전하게 분리하는 것을 권장합니다. (참고: React 공식 문서 - Effect에서 최신 props 및 state 읽기) 멋진 모달 중첩 로직이 더욱 견고해질 거예요! 😉

💡 제안하는 `useRef` 활용 수정안
   const [shouldRender, setShouldRender] = useState(isOpen);
   const [isVisible, setIsVisible] = useState(false);
   const [stackIndex, setStackIndex] = useState(0);
   const dialogRef = useRef<HTMLDivElement>(null);
   const wasFloatingLayerOpenRef = useRef(false);
+  const onExitedRef = useRef(onExited);
+  onExitedRef.current = onExited;
 
   useEffect(() => {
     if (!isOpen) {
       setIsVisible(false);
 
       const hideTimer = setTimeout(() => {
         setShouldRender(false);
-        onExited?.();
+        onExitedRef.current?.();
       }, EXIT_ANIMATION_DURATION);
 
       return () => clearTimeout(hideTimer);
     }
 
     setStackIndex(acquireModalStackIndex());
     setShouldRender(true);
 
     const showFrame = requestAnimationFrame(() => setIsVisible(true));
 
     return () => cancelAnimationFrame(showFrame);
-  }, [isOpen, onExited]);
+  }, [isOpen]);
📝 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
setStackIndex(acquireModalStackIndex());
const [shouldRender, setShouldRender] = useState(isOpen);
const [isVisible, setIsVisible] = useState(false);
const [stackIndex, setStackIndex] = useState(0);
const dialogRef = useRef<HTMLDivElement>(null);
const wasFloatingLayerOpenRef = useRef(false);
const onExitedRef = useRef(onExited);
onExitedRef.current = onExited;
useEffect(() => {
if (!isOpen) {
setIsVisible(false);
const hideTimer = setTimeout(() => {
setShouldRender(false);
onExitedRef.current?.();
}, EXIT_ANIMATION_DURATION);
return () => clearTimeout(hideTimer);
}
setStackIndex(acquireModalStackIndex());
setShouldRender(true);
const showFrame = requestAnimationFrame(() => setIsVisible(true));
return () => cancelAnimationFrame(showFrame);
}, [isOpen]);
🤖 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/components/modal/OverlayModal.tsx` at line 56, Update the
useEffect containing setStackIndex and acquireModalStackIndex so changes to the
onExited callback do not rerun the effect while the modal remains open. Store
the latest onExited callback in a ref or otherwise remove it safely from the
effect dependencies, while preserving the existing modal stack acquisition
behavior for genuine open-state changes.

setShouldRender(true);

const showFrame = requestAnimationFrame(() => setIsVisible(true));
Expand Down Expand Up @@ -71,13 +82,19 @@ export const OverlayModal = ({

if (!shouldRender) return null;

const overlayZIndex =
BASE_OVERLAY_Z_INDEX + stackIndex * MODAL_STACK_Z_INDEX_STEP;
const panelZIndex =
BASE_PANEL_Z_INDEX + stackIndex * MODAL_STACK_Z_INDEX_STEP;

return createPortal(
<>
<div
className={cn(
"bg-timo-overlay fixed inset-0 z-40 transition-opacity duration-200 ease-out",
"bg-timo-overlay fixed inset-0 transition-opacity duration-200 ease-out",
isVisible ? "opacity-100" : "opacity-0",
)}
style={{ zIndex: overlayZIndex }}
onPointerDownCapture={() => {
wasFloatingLayerOpenRef.current = hasOpenFloatingLayer();
}}
Expand All @@ -102,8 +119,9 @@ export const OverlayModal = ({
aria-modal="true"
aria-label={ariaLabel}
tabIndex={-1}
style={{ zIndex: panelZIndex }}
className={cn(
"fixed top-1/2 left-1/2 z-50 flex -translate-x-1/2 -translate-y-1/2 flex-col rounded-[4px] bg-white transition-all duration-200 ease-out",
"fixed top-1/2 left-1/2 flex -translate-x-1/2 -translate-y-1/2 flex-col rounded-[4px] bg-white transition-all duration-200 ease-out",
isVisible ? "scale-100 opacity-100" : "scale-95 opacity-0",
className,
)}
Expand Down
12 changes: 11 additions & 1 deletion apps/timo-web/hooks/timer/use-timer-query-invalidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

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

import { getGetHomeQueryKey } from "@/api/generated/endpoints/home/home";
import { getGetFocusTodoQueryKey } from "@/api/generated/endpoints/focus/focus";
import {
getGetHomeQueryKey,
getGetTodayQueryKey,
} from "@/api/generated/endpoints/home/home";
import { getGetTimeBoxesQueryKey } from "@/api/generated/endpoints/time-box/time-box";
import { getGetActiveTimerQueryKey } from "@/api/generated/endpoints/timer/timer";

Expand All @@ -15,6 +19,10 @@ export const useTimerQueryInvalidation = () => {
queryClient.invalidateQueries({ queryKey: getGetHomeQueryKey() });
const invalidateTimeBoxes = () =>
queryClient.invalidateQueries({ queryKey: getGetTimeBoxesQueryKey() });
const invalidateTodayView = () =>
queryClient.invalidateQueries({ queryKey: getGetTodayQueryKey() });
const invalidateFocusTodo = () =>
queryClient.invalidateQueries({ queryKey: getGetFocusTodoQueryKey() });
const invalidateTimerState = () => {
invalidateActiveTimer();
invalidateHomeView();
Expand All @@ -25,6 +33,8 @@ export const useTimerQueryInvalidation = () => {
invalidateActiveTimer,
invalidateHomeView,
invalidateTimeBoxes,
invalidateTodayView,
invalidateFocusTodo,
invalidateTimerState,
};
};
Loading
Loading