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
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 @@ -105,7 +105,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 @@ -165,7 +169,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 @@ -88,14 +88,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 @@ -155,7 +159,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 @@ -137,7 +137,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
2 changes: 1 addition & 1 deletion apps/timo-web/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@
"title": "Shall we stop now?",
"description": "The <blue>{minutes}</blue> completed so far will be recorded in the timebox.",
"continueButton": "Continue",
"switchButton": "Switch"
"switchButton": "Finish"
},
"completeModal": {
"title": "The task has been completed!",
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