Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
14895fb
feat(web): TodayTodoCard 컴포넌트 구현 (#101)
ehye1 Jul 6, 2026
f270c93
refactor(web): TodayTodoCard toolbar props 구조화 및 hover 로직 적용 (#101)
ehye1 Jul 6, 2026
ac806e3
feat(web): CreateTodoToolbar 컴포넌트 구현 및 TodayTodoCard에 통합 (#101)
ehye1 Jul 7, 2026
9e431b9
fix(ui): TagIcon 좌우 padding 피그마 스펙으로 수정 (#101)
ehye1 Jul 7, 2026
6c5811d
chore(ui): memo-blue 아이콘 소스 SVG 추가
ehye1 Jul 7, 2026
53cecbc
refactor(web): CreateTodoToolbar time 텍스트 width 클래스 단순화 (#101)
ehye1 Jul 7, 2026
fbbca26
Merge remote-tracking branch 'origin/develop' into feat/web/101-today…
ehye1 Jul 7, 2026
5e12005
fix(web): TodayTodoCard 경로를 locale 라우트 구조에 맞게 이동 (#101)
ehye1 Jul 7, 2026
d9678f8
fix(web): 코드리뷰 반영 — truncate 경계값·자동정지 알림·import alias·빈 태그칩 (#101)
ehye1 Jul 7, 2026
2718ed2
fix(ui): ModalButton stories 너비 클래스 수정
ehye1 Jul 7, 2026
f1d1c8c
chore: develop 머지 충돌 해결
ehye1 Jul 7, 2026
d595fbd
refactor(web): TodayTodoCard isDone·subTodos 내부 state 관리 (#101)
ehye1 Jul 7, 2026
e5018a8
chore(web): today 페이지에 TodayTodoCard mock 데이터 프리뷰 추가 (#101)
ehye1 Jul 7, 2026
787973d
chore(web): 불필요한 주석 제거 및 TODO 주석 추가 (#101)
ehye1 Jul 7, 2026
6ed513d
refactor(web): TodayTodoCard 컴포넌트·컨테이너 분리 및 타입 정리 (#101)
ehye1 Jul 8, 2026
0080c32
feat(ui): TagIcon disable variant 추가 및 tag 항상 표시 (#101)
ehye1 Jul 8, 2026
6d5a644
fix(web): timerStatus prop 변경 시 isPlaying 상태 동기화 (#101)
ehye1 Jul 8, 2026
3183d39
fix(web): TodayTodoCard 제목 말줄임표 처리 (#101)
ehye1 Jul 8, 2026
1ed7a9b
Merge remote-tracking branch 'origin/develop' into feat/web/101-today…
ehye1 Jul 8, 2026
7ed3d41
feat(web): TodayTodo 리스트 컨테이너 추가 및 단일 타이머 제약 구현 (#101)
ehye1 Jul 8, 2026
e0b80cb
feat(web): TodayTodoCard dimmed 상태 아이콘 처리 및 isDraggable 제거 (#101)
ehye1 Jul 8, 2026
281652d
fix(web): CreateTodoToolbarProps 불필요한 옵셔널 제거 (#101)
ehye1 Jul 9, 2026
8f2c8ce
refactor(web): TodayTodoCard Priority 타입명 PriorityTypes로 변경 (#101)
ehye1 Jul 9, 2026
49f1a39
refactor(web): toolbar boolean props에 has 접두사 적용 (#101)
ehye1 Jul 9, 2026
037b6c7
fix(web): CreateTodoToolbarProps tag 옵셔널 복구 (#101)
ehye1 Jul 9, 2026
7ea2637
fix(web): CreateTodoToolbarProps 클릭 핸들러 옵셔널 복구 (#101)
ehye1 Jul 9, 2026
6da9956
refactor(web): formatDate·formatDuration 인라인 함수를 공유 유틸로 분리 (#101)
ehye1 Jul 10, 2026
50cc380
refactor(web): TodayTodoCard use client 디렉티브 제거 (#101)
ehye1 Jul 10, 2026
07a5b88
refactor(web): SubTodo.id 타입을 number로 통일 (#101)
ehye1 Jul 10, 2026
7bcc57c
refactor(web): runningTodoId를 todos에서 파생시키고 subtaskId 변환 코드 제거 (#101)
ehye1 Jul 10, 2026
cb712b0
fix(web): TodayTodoCardContainer onSubTodoCheck 타입 오류 수정 (#101)
ehye1 Jul 10, 2026
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
@@ -0,0 +1,96 @@
"use client";

import { useEffect, useState } from "react";

import type { ReactNode } from "react";

import {
TodayTodoCard,
type SubTodo,
type TodayTodoCardToolbar,
} from "@/app/[locale]/(main)/today/_components/TodayTodoCard";

export interface TodayTodoCardContainerProps {
title: string;
isDone: boolean;
subTodos: SubTodo[];
toolbar: TodayTodoCardToolbar;
timerStatus: "RUNNING" | "PAUSED" | "STOPPED";
icon?: ReactNode;
onIconClick?: () => void;
onCheck?: () => void;
onPlay?: () => void;
onDelete?: () => void;
onSubTodoCheck?: (id: number) => void;
}

export const TodayTodoCardContainer = ({
title,
isDone: initialIsDone,
icon,
onIconClick,
subTodos: initialSubTodos,
toolbar,
timerStatus,
onCheck,
onPlay,
onDelete,
onSubTodoCheck,
}: TodayTodoCardContainerProps) => {
const [isHovered, setIsHovered] = useState(false);
const [isPlaying, setIsPlaying] = useState(timerStatus === "RUNNING");
const [isDone, setIsDone] = useState(initialIsDone);
const [subTodos, setSubTodos] = useState(initialSubTodos);

useEffect(() => {
setIsPlaying(timerStatus === "RUNNING");
}, [timerStatus]);

const isDimmed = isDone && !isHovered;

const handleCheck = () => {
const next = !isDone;
setIsDone(next);
if (next) {
setSubTodos((prev) => prev.map((s) => ({ ...s, isDone: true })));
if (isPlaying) {
setIsPlaying(false);
onPlay?.();
}
}
onCheck?.();
};
Comment on lines +51 to +62

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 | 🔵 Trivial | 💤 Low value

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file outline =="
ast-grep outline "apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayTodoCardContainer.tsx" --view expanded || true

echo "== relevant file lines =="
sed -n '1,180p' "apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayTodoCardContainer.tsx"

echo "== search usages of TodayTodoCardContainer and onPlay =="
rg -n "TodayTodoCardContainer|onPlay|onStop|onCheck" "apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today" -S

Repository: Team-Timo/Timo-client

Length of output: 5339


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== TodayTodoCard component outline =="
ast-grep outline "apps/timo-web/app/[locale]/(main)/today/_components/TodayTodoCard.tsx" --view expanded || true

echo "== TodayTodoCard component lines =="
sed -n '1,220p' "apps/timo-web/app/[locale]/(main)/today/_components/TodayTodoCard.tsx"

echo "== today-todo-mock lines =="
sed -n '1,220p' "apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_mocks/today-todo-mock.ts"

echo "== page lines =="
sed -n '1,120p' "apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/page.tsx"

Repository: Team-Timo/Timo-client

Length of output: 7540


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "\bonPlay\b|\bonStop\b|\bonTimerToggle\b|\bPlayButton\b" apps/timo-web -S

Repository: Team-Timo/Timo-client

Length of output: 1645


onPlay 이름을 더 중립적으로 바꾸세요.
구조는 깔끔한데, 이름만 살짝 더 맞추면 읽기 쉬워져요. 지금은 재생 버튼 클릭과 체크로 인한 자동 정지를 같은 콜백으로 전달해서, onPlay보다 onTimerToggle 같은 이름이 의도를 더 잘 드러냅니다. 정지 동작을 따로 다뤄야 하면 onStop을 분리하는 쪽도 좋습니다.
React 이벤트 패턴: https://react.dev/learn/responding-to-events

🤖 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)/today/_containers/TodayTodoCardContainer.tsx
around lines 49 - 57, Rename the callback in TodayTodoCardContainer’s
handleCheck flow so the prop name reflects the neutral timer state change rather
than playback only; update the local usage of onPlay and its prop definition to
something like onTimerToggle, or split out a dedicated onStop if you need
separate stop behavior. Make sure all references in TodayTodoCardContainer stay
consistent so the check-driven auto-stop and any button-triggered action use the
updated, clearer callback name.


const handlePlay = () => {
if (!isDone) {
setIsPlaying((prev) => !prev);
onPlay?.();
}
};

const handleSubTodoCheck = (id: number) => {
setSubTodos((prev) =>
prev.map((s) => (s.id === id ? { ...s, isDone: !s.isDone } : s)),
);
onSubTodoCheck?.(id);
};

return (
<TodayTodoCard
title={title}
isDone={isDone}
isDimmed={isDimmed}
isPlaying={isPlaying}
icon={icon}
onIconClick={onIconClick}
subTodos={subTodos}
toolbar={toolbar}
onCheck={handleCheck}
onPlay={handlePlay}
onDelete={onDelete ?? (() => {})}
onSubTodoCheck={handleSubTodoCheck}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"use client";

import { useState } from "react";

import type { TodoMock } from "@/app/[locale]/(main)/(with-time-sidebar)/today/_mocks/today-todo-mock";

import { TodayTodoCardContainer } from "@/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayTodoCardContainer";
import { todayTodoMocks } from "@/app/[locale]/(main)/(with-time-sidebar)/today/_mocks/today-todo-mock";
import { convertDurationToTimeText } from "@/utils/convert-duration-to-time-text";
import { formatDate } from "@/utils/format-date";

const PRIORITY_MAP = {
URGENT: "urgent",
HIGH: "high",
MEDIUM: "medium",
LOW: "low",
} as const;

export const TodayTodoListContainer = () => {
const [todos, setTodos] = useState<TodoMock[]>(todayTodoMocks);
const runningTodoId =
todos.find((t) => t.timerStatus === "RUNNING")?.todoId ?? null;

const handlePlay = (todoId: number) => {
// TODO: API
setTodos((prev) =>
prev.map((t) => ({
...t,
timerStatus:
t.todoId === todoId && t.timerStatus !== "RUNNING"
? "RUNNING"
: "STOPPED",
})),
);
};

const handleCheck = (todoId: number) => {
// TODO: API
setTodos((prev) =>
prev.map((t) =>
t.todoId === todoId
? { ...t, completed: !t.completed, timerStatus: "STOPPED" }
: t,
),
);
};

const handleDelete = (todoId: number) => {
// TODO: API
setTodos((prev) => prev.filter((t) => t.todoId !== todoId));
};

const handleSubTodoCheck = (todoId: number, subtaskId: number) => {
// TODO: API
setTodos((prev) =>
prev.map((t) =>
t.todoId === todoId
? {
...t,
subtasks: t.subtasks.map((s) =>
s.subtaskId === subtaskId
? { ...s, completed: !s.completed }
: s,
),
}
: t,
),
);
};

return (
<div className="flex flex-col gap-2">
{todos.map((todo) => (
<TodayTodoCardContainer
key={todo.todoId}
title={todo.title}
isDone={todo.completed}
timerStatus={runningTodoId === todo.todoId ? "RUNNING" : "STOPPED"}
toolbar={{
date: formatDate(todo.date),
time: convertDurationToTimeText(todo.durationSeconds),
priority: PRIORITY_MAP[todo.priority],
tag: todo.tag?.name,
hasMemo: todo.hasMemo,
hasRepeat: todo.isRepeated,
}}
subTodos={todo.subtasks.map((s) => ({
id: s.subtaskId,
text: s.content,
isDone: s.completed,
}))}
onPlay={() => handlePlay(todo.todoId)}
onCheck={() => handleCheck(todo.todoId)}
onDelete={() => handleDelete(todo.todoId)}
onSubTodoCheck={(id) => handleSubTodoCheck(todo.todoId, id)}
/>
))}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
interface TodoTag {
tagId: number;
name: string;
}

interface TodoSubtask {
subtaskId: number;
content: string;
completed: boolean;
}

export interface TodoMock {
todoId: number;
icon: string;
title: string;
completed: boolean;
date: string;
durationSeconds: number;
priority: "URGENT" | "HIGH" | "MEDIUM" | "LOW";
tag: TodoTag | null;
hasMemo: boolean;
isRepeated: boolean;
timerStatus: "RUNNING" | "PAUSED" | "STOPPED";
sortOrder: number;
subtasks: TodoSubtask[];
}
Comment on lines +1 to +26

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

인터페이스들 나중에 API 연동 시에는 zod 도입해 봅시다!


export const todayTodoMocks: TodoMock[] = [
{
todoId: 1,
icon: "ICON_1",
title: "디자인 시스템 컴포넌트 정리하기",
completed: false,
date: "2026-07-09",
durationSeconds: 36000,
priority: "URGENT",
tag: { tagId: 1, name: "작업" },
hasMemo: true,
isRepeated: false,
timerStatus: "STOPPED",
sortOrder: 0,
subtasks: [
{ subtaskId: 1, content: "색상 토큰 정리", completed: true },
{ subtaskId: 2, content: "타이포그래피 스펙 문서화", completed: false },
],
},
{
todoId: 2,
icon: "ICON_2",
title: "완료된 할 일 예시",
completed: true,
date: "2026-07-09",
durationSeconds: 3600,
priority: "MEDIUM",
tag: { tagId: 2, name: "완료" },
hasMemo: false,
isRepeated: false,
timerStatus: "STOPPED",
sortOrder: 1,
subtasks: [],
},
{
todoId: 3,
icon: "ICON_3",
title: "서브투두 없는 단순 카드",
completed: false,
date: "2026-07-09",
durationSeconds: 1800,
priority: "HIGH",
tag: null,
hasMemo: true,
isRepeated: true,
timerStatus: "STOPPED",
sortOrder: 2,
subtasks: [],
},
];
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { TodayHeaderContainer } from "@/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayHeaderContainer";
import { TodayTodoListContainer } from "@/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayTodoListContainer";

export default function TodayPage() {
return <TodayHeaderContainer />;
return (
<section>
<TodayHeaderContainer />
<div className="p-4">
<TodayTodoListContainer />
</div>
</section>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ import {
convertDateToDayNumberText,
convertDateToDayOfWeekText,
} from "@/app/[locale]/(main)/focus/_utils/date";
import { convertDurationToTimeText } from "@/app/[locale]/(main)/focus/_utils/duration";
import { Timer } from "@/components/timer/Timer";
import { TimerControls } from "@/components/timer/TimerControls";
import { convertDurationToTimeText } from "@/utils/convert-duration-to-time-text";

export const FocusSessionContainer = () => {
const [task, setTask] = useState<FocusTask>(focusTaskMock);
Expand Down
Loading
Loading