Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
6 changes: 6 additions & 0 deletions apps/timo-web/api/generated/endpoints/timer/timer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ const withQueryKey = <T extends object, K>(
/**
* 투두의 타이머를 시작합니다.
* 한 번에 한 개의 타이머만 실행 가능하며, 이미 실행/일시정지 중인 타이머가 있으면 409를 반환합니다.
*
* 응답의 date는 타이머가 귀속되는 날짜(사용자 시간대 기준 시작일)입니다.
* 반복 투두는 여러 날짜에 같은 todoId로 노출되므로, 실행중 표시는 todoId가 아닌 (todoId, date) 조합으로 구분해야 합니다.
* @summary 타이머 시작
*/
export const startTimer = (
Expand Down Expand Up @@ -499,6 +502,9 @@ export const useCompleteTimer = <
* 로그인한 사용자의 현재 실행 중(RUNNING/PAUSED)인 타이머를 단건 조회합니다.
* 한 사용자당 시작 이후 완료/종료되지 않은 타이머는 최대 1개만 존재할 수 있습니다.
* 실행 중인 타이머가 없으면 data: null을 반환합니다.
*
* 응답의 date는 타이머가 귀속되는 날짜(사용자 시간대 기준 시작일)입니다.
* 반복 투두는 여러 날짜에 같은 todoId로 노출되므로, 실행중 표시는 todoId가 아닌 (todoId, date) 조합으로 구분해야 합니다.
* @summary 현재 실행 중인 타이머 조회
*/
export const getActiveTimer = (
Expand Down
8 changes: 8 additions & 0 deletions apps/timo-web/api/generated/endpoints/timer/timer.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import * as zod from "zod";
/**
* 투두의 타이머를 시작합니다.
* 한 번에 한 개의 타이머만 실행 가능하며, 이미 실행/일시정지 중인 타이머가 있으면 409를 반환합니다.
*
* 응답의 date는 타이머가 귀속되는 날짜(사용자 시간대 기준 시작일)입니다.
* 반복 투두는 여러 날짜에 같은 todoId로 노출되므로, 실행중 표시는 todoId가 아닌 (todoId, date) 조합으로 구분해야 합니다.
* @summary 타이머 시작
*/
export const StartTimerParams = zod.object({
Expand All @@ -23,6 +26,7 @@ export const StartTimerResponse = zod.object({
.object({
timerId: zod.number(),
todoId: zod.number(),
date: zod.string(),
status: zod.string(),
plannedSeconds: zod.number(),
startedAt: zod.string(),
Expand Down Expand Up @@ -153,6 +157,9 @@ export const CompleteTimerResponse = zod.object({
* 로그인한 사용자의 현재 실행 중(RUNNING/PAUSED)인 타이머를 단건 조회합니다.
* 한 사용자당 시작 이후 완료/종료되지 않은 타이머는 최대 1개만 존재할 수 있습니다.
* 실행 중인 타이머가 없으면 data: null을 반환합니다.
*
* 응답의 date는 타이머가 귀속되는 날짜(사용자 시간대 기준 시작일)입니다.
* 반복 투두는 여러 날짜에 같은 todoId로 노출되므로, 실행중 표시는 todoId가 아닌 (todoId, date) 조합으로 구분해야 합니다.
* @summary 현재 실행 중인 타이머 조회
*/
export const GetActiveTimerResponse = zod.object({
Expand All @@ -162,6 +169,7 @@ export const GetActiveTimerResponse = zod.object({
.object({
timerId: zod.number(),
todoId: zod.number(),
date: zod.string(),
todoTitle: zod.string(),
iconType: zod.string().optional(),
status: zod.string(),
Expand Down
1 change: 1 addition & 0 deletions apps/timo-web/api/generated/models/timerActiveResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
export interface TimerActiveResponse {
timerId: number;
todoId: number;
date: string;
todoTitle: string;
iconType?: string;
status: string;
Expand Down
1 change: 1 addition & 0 deletions apps/timo-web/api/generated/models/timerStartResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
export interface TimerStartResponse {
timerId: number;
todoId: number;
date: string;
status: string;
plannedSeconds: number;
startedAt: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,9 @@ export const HomeTodoContainer = () => {
{todos.map((todo) => {
const [firstSubtask] = todo.subtasks;
const isActiveTodo =
activeTimer && activeTimer.todoId === todo.todoId;
activeTimer &&
activeTimer.todoId === todo.todoId &&
activeTimer.date === dateKey;
const durationSeconds = isActiveTodo
? activeTimer.plannedSeconds + activeTimer.extendedSeconds
: todo.durationSeconds;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,11 @@ export const useHomeTodosByDate = (
todoId: number,
completed: boolean,
) => {
if (completed && activeTimer?.todoId === todoId) {
if (
completed &&
activeTimer?.todoId === todoId &&
activeTimer.date === dateKey
) {
Comment on lines +108 to +112

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

activeTimer 타입 좁히기(Type Narrowing) 및 코드 일관성 개선

동일한 파일 내 다른 핸들러(예: handleTogglePlay)에서는 activeTimer && activeTimer.todoId === todoId 형태로 안전하게 타입을 좁히고 있습니다. 반면 아래 코드들에서는 옵셔널 체이닝(?.)을 사용한 후 다시 activeTimer.date에 직접 접근하고 있어 코드의 일관성이 떨어지며, TypeScript 환경에 따라 타입 좁히기가 완벽하게 동작하지 않아 컴파일 에러가 발생할 여지가 있습니다. (참고: TypeScript: Type Narrowing)
안전하고 통일성 있는 코드를 위해 명시적 Null 체크 방식으로 맞춰주시는 것을 추천해요! 🛠️

  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/use-home-todos-by-date.ts#L98-L102: activeTimer && activeTimer.todoId === todoId && activeTimer.date === dateKey 구조로 수정하세요.
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_hooks/useTodayTodoList.ts#L84-L88: 위와 동일하게 수정하세요.
  • apps/timo-web/containers/todo-modal/detail/DetailTodoModalContainer.tsx#L75-L77: activeTimer && activeTimer.todoId === todoId && activeTimer.date === date 구조로 수정하세요.
📍 Affects 3 files
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/use-home-todos-by-date.ts#L98-L102 (this comment)
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_hooks/useTodayTodoList.ts#L84-L88
  • apps/timo-web/containers/todo-modal/detail/DetailTodoModalContainer.tsx#L75-L77
🤖 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)/(with-time-sidebar)/home/_hooks/use-home-todos-by-date.ts
around lines 98 - 102, The activeTimer condition must explicitly narrow the
nullable value before accessing its properties. Update the condition in
apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/use-home-todos-by-date.ts
lines 98-102,
apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_hooks/useTodayTodoList.ts
lines 84-88, and
apps/timo-web/containers/todo-modal/detail/DetailTodoModalContainer.tsx lines
75-77 to use activeTimer && followed by the todoId and date comparisons,
preserving the existing completed and matching-date behavior.

onNeedStopConfirm(dateKey, todoId);
return;
}
Expand Down Expand Up @@ -155,7 +159,11 @@ export const useHomeTodosByDate = (
openTimerPanel();
}

if (activeTimer && activeTimer.todoId === todoId) {
if (
activeTimer &&
activeTimer.todoId === todoId &&
activeTimer.date === dateKey
) {
changeStatus(
{
timerId: activeTimer.timerId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,9 @@ export const TodayTodoListContainer = () => {
<div className="flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto pr-1 pb-4">
{todos.map((todo) => {
const isActiveTodo =
activeTimer && activeTimer.todoId === todo.todoId;
activeTimer &&
activeTimer.todoId === todo.todoId &&
activeTimer.date === todo.date;
const durationSeconds = isActiveTodo
? activeTimer.plannedSeconds + activeTimer.extendedSeconds
: todo.durationSeconds;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,18 @@ export const useTodayTodoList = (
};

const handleToggleCompleted = (todoId: number, completed: boolean) => {
if (completed && activeTimer?.todoId === todoId) {
const dateKey = todos.find((todo) => todo.todoId === todoId)?.date;
if (!dateKey) return;

if (
completed &&
activeTimer?.todoId === todoId &&
activeTimer.date === dateKey
) {
onNeedStopConfirm(todoId);
return;
}

const dateKey = todos.find((todo) => todo.todoId === todoId)?.date;
if (!dateKey) return;

const previous = todos;
updateTodo(todoId, (todo) => ({ ...todo, completed }));

Expand Down Expand Up @@ -143,7 +147,11 @@ export const useTodayTodoList = (
openTimerPanel();
}

if (activeTimer && activeTimer.todoId === todoId) {
if (
activeTimer &&
activeTimer.todoId === todoId &&
activeTimer.date === dateKey
) {
changeStatus(
{
timerId: activeTimer.timerId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,10 @@ export const useFocusSession = ({

const todo = focusView.todo;
const timer =
activeTimer && todo && activeTimer.todoId === todo.todoId
activeTimer &&
todo &&
activeTimer.todoId === todo.todoId &&
activeTimer.date === focusView.date
? activeTimer
: undefined;
const isRunning = timer?.status === "RUNNING";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ const DetailTodoModalQuery = ({
const { handleToggle } = useToggleSubtaskSubmit();
const { data: activeTimer } = useActiveTimer();
const todo = data?.data;
const isPlayHighlighted = !activeTimer || activeTimer.todoId === todoId;
const isPlayHighlighted =
!activeTimer ||
(activeTimer.todoId === todoId && activeTimer.date === date);

useEffect(() => {
if (isError && error) {
Expand All @@ -70,7 +72,9 @@ const DetailTodoModalQuery = ({
if (isError || !todo) return null;

const timerStatus =
activeTimer?.todoId === todoId ? activeTimer.status : todo.timerStatus;
activeTimer?.todoId === todoId && activeTimer.date === date
? activeTimer.status
: todo.timerStatus;

const deleteTodo = () => {
handleDelete(todoId, {
Expand Down
1 change: 1 addition & 0 deletions apps/timo-web/schemas/timer/timer-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { z } from "zod";
export const activeTimerSchema = z.object({
timerId: z.number(),
todoId: z.number(),
date: z.string(),
iconType: z
.enum(TODO_ICON_VALUES)
.nullish()
Expand Down
Loading