Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
6bcb68b
feat(web): 기존 투두 상세 모달 진입점 연결 (#151)
yumin-kim2 Jul 12, 2026
7b17cef
refactor(web): 투두 아이콘 필드 공통화 (#151)
yumin-kim2 Jul 12, 2026
fc3494d
feat(web): 상세 투두 모달 기본 레이아웃 구성 (#151)
yumin-kim2 Jul 12, 2026
354eb92
refactor(web): 상세 투두 본문 영역 컴포넌트 분리 (#151)
yumin-kim2 Jul 12, 2026
bbee8f3
feat(web): 상세 투두 메모 영역 추가 (#151)
yumin-kim2 Jul 12, 2026
3c02616
feat(web): 툴바 컴포넌트 임시값으로 연결 (#151)
yumin-kim2 Jul 12, 2026
fdcbf65
feat(web): 상세 투두 툴바 상태 변경 연결 (#151)
yumin-kim2 Jul 12, 2026
1c9b19e
refactor(web): 상세 투두 폼 상태 훅 분리 (#151)
yumin-kim2 Jul 12, 2026
38abc3d
feat(web): 상세 투두 입력 상태 보완 (#151)
yumin-kim2 Jul 12, 2026
78f98d3
Merge remote-tracking branch 'origin/develop' into feat/web/151-detai…
yumin-kim2 Jul 12, 2026
796de1f
feat(web): 상세 투두 실행 버튼 동작 연결 (#151)
yumin-kim2 Jul 12, 2026
1fca154
feat(web): 상세 투두 제목 및 하위 태스크 수정 연결 (#151)
yumin-kim2 Jul 12, 2026
ab7a493
feat(web): 상세 투두 삭제 동작 연결 (#151)
yumin-kim2 Jul 12, 2026
86daee3
feat(web): 투두 툴바 라벨 주입 지원 (#151)
yumin-kim2 Jul 12, 2026
b128a11
feat(web): 투두 모달 다국어 문구 적용 (#151)
yumin-kim2 Jul 12, 2026
83fe429
refactor(web): margin값을 제외한 레이아웃 재구성 (#151)
yumin-kim2 Jul 13, 2026
6925613
refactor(web): 투두 유틸 공용 경로로 이동 (#151)
yumin-kim2 Jul 13, 2026
cd0c888
refactor(web): 투두 모달 훅 공용 경로로 이동 (#151)
yumin-kim2 Jul 13, 2026
23daadb
refactor(web): 투두 모달 컴포넌트 공용 경로로 이동 (#151)
yumin-kim2 Jul 13, 2026
c763069
refactor(web): 태그 라벨 판별 유틸 공용화 (#151)
yumin-kim2 Jul 13, 2026
6accb01
refactor(web): 상세 투두 모달 다국어 요일 처리 (#151)
yumin-kim2 Jul 13, 2026
90eea50
style(web): 상세 투두 하위 태스크 간격 통일 (#151)
yumin-kim2 Jul 13, 2026
df8fadf
refactor(web): 상세 투두 폼 상태 관리 react-hook-form으로 개선 (#151)
yumin-kim2 Jul 13, 2026
1370156
fix(web): 투두 카드 키보드 이벤트 전파 방지 (#151)
yumin-kim2 Jul 13, 2026
c715de1
merge: develop 최신 변경 반영
yumin-kim2 Jul 13, 2026
14343d0
merge: develop 최신 변경 재반영
yumin-kim2 Jul 13, 2026
8117609
refactor(web): 생성 모달 컨테이너와 날짜 포맷 유틸 공용화 (#151)
yumin-kim2 Jul 14, 2026
9d75ebc
refactor(web): 상세 투두 하위 태스크 로직 분리 (#151)
yumin-kim2 Jul 14, 2026
9470836
refactor(web): 상세 투두 UI 상태를 모달 컨텐츠로 분리 (#151)
yumin-kim2 Jul 14, 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
Expand Up @@ -22,8 +22,9 @@ import type {
TodoPriorityTypes,
TodoTimerStatusTypes,
} from "@/app/[locale]/(main)/(with-time-sidebar)/home/_types/todo-type";
import type { KeyboardEvent, MouseEvent } from "react";

import { convertDurationToTimeText } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_utils/todo-time";
import { convertDurationToTimeText } from "@/utils/todo/todo-time";

type PriorityLabelKeyTypes = "urgent" | "high" | "medium" | "low";

Expand All @@ -34,6 +35,10 @@ const PRIORITY_LABEL_KEY: Record<TodoPriorityTypes, PriorityLabelKeyTypes> = {
LOW: "low",
};

const isInteractiveElement = (target: EventTarget | null) =>
target instanceof HTMLElement &&
Boolean(target.closest("button, input, label"));

export interface HomeTodoCardProps {
todoId: number;
title: string;
Expand All @@ -46,6 +51,7 @@ export interface HomeTodoCardProps {
timerStatus: TodoTimerStatusTypes;
subtaskTitle?: string;
isSubtaskCompleted?: boolean;
onClickTodo?: () => void;
onToggleCompleted: (completed: boolean) => void;
onTogglePlay: () => void;
onToggleSubtaskCompleted?: (completed: boolean) => void;
Expand All @@ -63,6 +69,7 @@ export const HomeTodoCard = ({
timerStatus,
subtaskTitle,
isSubtaskCompleted = false,
onClickTodo,
onToggleCompleted,
onTogglePlay,
onToggleSubtaskCompleted,
Expand All @@ -81,6 +88,21 @@ export const HomeTodoCard = ({

const priorityLabel = tCommon(`priority.${PRIORITY_LABEL_KEY[priority]}`);

const handleCardClick = (event: MouseEvent<HTMLDivElement>) => {
if (isInteractiveElement(event.target)) return;

onClickTodo?.();
};

const handleCardKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
if (isInteractiveElement(event.target)) return;
if (!onClickTodo) return;
if (event.key !== "Enter" && event.key !== " ") return;

event.preventDefault();
onClickTodo();
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const titleRow = (
<div className="flex w-full items-center justify-between gap-2">
<div className="flex min-w-0 flex-1 items-center gap-1">
Expand Down Expand Up @@ -112,14 +134,19 @@ export const HomeTodoCard = ({
);

return (
<article
<div
ref={setNodeRef}
style={sortableStyle}
{...attributes}
{...listeners}
role={onClickTodo ? "button" : attributes.role}
tabIndex={onClickTodo ? 0 : attributes.tabIndex}
onClick={onClickTodo ? handleCardClick : undefined}
onKeyDown={onClickTodo ? handleCardKeyDown : undefined}
className={cn(
"border-timo-gray-500 flex w-full shrink-0 flex-col items-start gap-2 overflow-hidden rounded-[4px] border border-solid px-3.5 py-3",
isCompleted ? "bg-timo-gray-200" : "bg-white",
onClickTodo && "cursor-pointer",
)}
>
Comment on lines +137 to 151

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 | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the target file and nearby related components
git ls-files 'apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_components/todo-card/*' \
  'apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/**/*todo-card*' \
  'apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/**/*TodoCard*'

# Show the target file with line numbers if present
target='apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_components/todo-card/HomeTodoCard.tsx'
if [ -f "$target" ]; then
  sed -n '1,260p' "$target" | cat -n
fi

# Find keyboard / click handling and interactive descendants in the home todo card area
rg -n --context 3 'role="button"|tabIndex=0|onKeyDown|onClickTodo|checkbox|button|play|aria-label' \
  'apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home' \
  'apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_components' \
  'apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers' \
  'apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_hooks' \
  || true

Repository: Team-Timo/Timo-client

Length of output: 50377


카드 전체를 버튼으로 만들기보다 내부 컨트롤과 분리해 주세요

role="button"/tabIndex=0를 카드 전체에 주면 CheckboxPlayButton이 같은 인터랙션 트리 안에 들어가 탭 순서와 역할이 겹칩니다. WAI-ARIA APG도 이런 중첩 패턴은 피하라고 권장합니다: https://www.w3.org/WAI/ARIA/apg/

상세 진입이 필요하면 카드 자체는 드래그/표시용으로 두고, 별도의 상세보기 버튼으로 분리하는 쪽이 안전합니다. 버튼이 많은 카드일수록 역할 구분이 또렷해야 합니다. 😄

🤖 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/_components/todo-card/HomeTodoCard.tsx
around lines 134 - 148, Remove the card-level button semantics and
keyboard/click handling from the sortable container in HomeTodoCard, including
role, tabIndex, onClick, and onKeyDown tied to onClickTodo. Keep the card as a
drag/display container and add or use a separate accessible detail button for
onClickTodo, ensuring Checkbox and PlayButton remain independent controls with
distinct roles and tab order.

{subtaskTitle ? (
Expand Down Expand Up @@ -167,6 +194,6 @@ export const HomeTodoCard = ({
{convertDurationToTimeText(durationSeconds)}
</span>
</div>
</article>
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,10 @@ import {
import { useHomeTodosByDate } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/use-home-todos-by-date";
import { useHomeViewMode } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/use-home-view-mode";
import { useHomeView } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_queries/use-home-view";
import { DetailTodoModalContainer } from "@/components/todo-modal/detail/DetailTodoModalContainer";
import { DndSortableListProvider } from "@/providers/dnd/DndSortableListProvider";
import { formatDateKey } from "@/utils/date/date";

const TAG_LABEL_KEYS = [
"dailyLife",
"work",
"exercise",
"assignment",
"additional",
] as const;

type TagLabelKey = (typeof TAG_LABEL_KEYS)[number];

const isTagLabelKey = (value: string): value is TagLabelKey =>
(TAG_LABEL_KEYS as readonly string[]).includes(value);
import { isTagLabelKey } from "@/utils/todo/tag-label";

export const HomeTodoContainer = () => {
const tCommon = useTranslations("Common");
Expand All @@ -48,6 +37,7 @@ export const HomeTodoContainer = () => {
handleToggleCompleted,
handleTogglePlay,
handleToggleSubtaskCompleted,
handleDeleteTodo,
handleReorderTodo,
} = useHomeTodosByDate(days);

Expand Down Expand Up @@ -100,42 +90,57 @@ export const HomeTodoContainer = () => {
const [firstSubtask] = todo.subtasks;

return (
<HomeTodoCard
<DetailTodoModalContainer
key={todo.todoId}
todoId={todo.todoId}
title={todo.title}
isCompleted={todo.completed}
durationSeconds={todo.durationSeconds}
priority={todo.priority}
tagName={
todo.tag &&
(isTagLabelKey(todo.tag.name)
? tCommon(`tag.${todo.tag.name}`)
: todo.tag.name)
}
hasMemo={todo.hasMemo}
isRepeated={todo.isRepeated}
timerStatus={todo.timerStatus}
subtaskTitle={firstSubtask?.content}
isSubtaskCompleted={firstSubtask?.completed}
onToggleCompleted={(completed) =>
handleToggleCompleted(dateKey, todo.todoId, completed)
}
todo={todo}
onTogglePlay={() =>
handleTogglePlay(dateKey, todo.todoId)
}
onToggleSubtaskCompleted={
firstSubtask
? (completed) =>
handleToggleSubtaskCompleted(
dateKey,
todo.todoId,
firstSubtask.subtaskId,
completed,
)
: undefined
}
/>
onDelete={() => handleDeleteTodo(dateKey, todo.todoId)}
>
{(openDetailTodoModal) => (
<HomeTodoCard
todoId={todo.todoId}
title={todo.title}
isCompleted={todo.completed}
durationSeconds={todo.durationSeconds}
priority={todo.priority}
tagName={
todo.tag &&
(isTagLabelKey(todo.tag.name)
? tCommon(`tag.${todo.tag.name}`)
: todo.tag.name)
}
hasMemo={todo.hasMemo}
isRepeated={todo.isRepeated}
timerStatus={todo.timerStatus}
subtaskTitle={firstSubtask?.content}
isSubtaskCompleted={firstSubtask?.completed}
onClickTodo={openDetailTodoModal}
onToggleCompleted={(completed) =>
handleToggleCompleted(
dateKey,
todo.todoId,
completed,
)
}
onTogglePlay={() =>
handleTogglePlay(dateKey, todo.todoId)
}
onToggleSubtaskCompleted={
firstSubtask
? (completed) =>
handleToggleSubtaskCompleted(
dateKey,
todo.todoId,
firstSubtask.subtaskId,
completed,
)
: undefined
}
/>
)}
</DetailTodoModalContainer>
);
})}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { useTranslations } from "next-intl";
import type { ApiDayOfWeek } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_types/home-view-type";

import { DateInformation } from "@/app/[locale]/(main)/(with-time-sidebar)/_components/DateInformation";
import { CreateTodoModalContainer } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_containers/todo-modal/CreateTodoModalContainer";
import { convertDateToDateText } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_utils/date";
import { CreateTodoModalContainer } from "@/components/todo-modal/create/CreateTodoModalContainer";
import { getToday, parseDateKey } from "@/utils/date/date";

export interface HomeDayHeaderContainerProps {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,14 @@ export const useHomeTodosByDate = (apiDays: HomeViewDay[]) => {
);
};

const handleDeleteTodo = (dateKey: string, todoId: number) => {
// TODO: API
setTodosByDate((prev) => ({
...prev,
[dateKey]: (prev[dateKey] ?? []).filter((todo) => todo.todoId !== todoId),
}));
};

const handleReorderTodo = (
dateKey: string,
fromIndex: number,
Expand Down Expand Up @@ -136,6 +144,7 @@ export const useHomeTodosByDate = (apiDays: HomeViewDay[]) => {
handleToggleCompleted,
handleTogglePlay,
handleToggleSubtaskCompleted,
handleDeleteTodo,
handleReorderTodo,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import type { TodoMock } from "@/app/[locale]/(main)/(with-time-sidebar)/today/_

import { DateInformation } from "@/app/[locale]/(main)/(with-time-sidebar)/_components/DateInformation";
import { convertDateToDateText } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_utils/date";
import { CreateTodoModalContainer } from "@/app/[locale]/(main)/(with-time-sidebar)/today/_containers/todo-modal/CreateTodoModalContainer";
import { useCreateTodoSubmit } from "@/app/[locale]/(main)/(with-time-sidebar)/today/_hooks/todo-modal/use-create-todo-submit";
import { CreateTodoModalContainer } from "@/components/todo-modal/create/CreateTodoModalContainer";
import { getToday } from "@/utils/date/date";
import { getDayOfWeekKey } from "@/utils/date/get-day-of-week-key";

Expand All @@ -27,6 +28,7 @@ export const TodayDateHeaderContainer = ({
const date = convertDateToDateText(today);
const dayKey = getDayOfWeekKey(today);
const dayOfWeek = tCommon(`weekday.${dayKey}`);
const { handleSubmit } = useCreateTodoSubmit({ onCreate: onCreateTodo });

return (
<div className="flex flex-col gap-3 pb-2">
Expand All @@ -38,7 +40,11 @@ export const TodayDateHeaderContainer = ({
totalCount={totalCount}
completedCount={completedCount}
/>
<CreateTodoModalContainer defaultDate={today} onCreate={onCreateTodo} />
<CreateTodoModalContainer
defaultDate={today}
buttonVariant="big"
onSubmit={handleSubmit}
/>
</div>
);
};

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { cn } from "@repo/timo-design-system/utils";

import type { TodoIconValue } from "@repo/timo-design-system/ui";

export interface CreateTodoIconFieldProps {
export interface TodoIconFieldProps {
Comment thread
yumin-kim2 marked this conversation as resolved.
icon: TodoIconValue | null;
isIconPanelOpen: boolean;
addIconLabel: string;
Expand All @@ -14,15 +14,15 @@ export interface CreateTodoIconFieldProps {
onRemoveIcon: () => void;
}

export const CreateTodoIconField = ({
export const TodoIconField = ({
icon,
isIconPanelOpen,
addIconLabel,
onOpenPanel,
onTogglePanel,
onSelectIcon,
onRemoveIcon,
}: CreateTodoIconFieldProps) => {
}: TodoIconFieldProps) => {
return (
<div className="flex w-full flex-col items-start gap-2">
{!isIconPanelOpen && icon ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,28 @@ import { AddTaskButton } from "@repo/timo-design-system/ui";
import { useTranslations } from "next-intl";
import { overlay } from "overlay-kit";

import { useCreateTodoSubmit } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/todo-modal/use-create-todo-submit";
import type { CreateTodoRequest } from "@/api/todo/todo-schema";

import { AnimatedToast } from "@/components/toast/AnimatedToast";
import { CreateTodoModalContent } from "@/components/todo-modal/CreateTodoModalContent";
import { CreateTodoModalContent } from "@/components/todo-modal/create/CreateTodoModalContent";
import { useCreateTodoSubmit } from "@/hooks/todo-modal/use-create-todo-submit";

export interface CreateTodoModalContainerProps {
defaultDate?: Date;
buttonVariant?: "default" | "big";
onSubmit?: (data: CreateTodoRequest) => void;
}

export const CreateTodoModalContainer = ({
defaultDate,
buttonVariant = "default",
onSubmit,
}: CreateTodoModalContainerProps) => {
const t = useTranslations("Home");
const tToast = useTranslations("Toast");
const { handleSubmit, isErrorToastOpen, closeErrorToast } =
useCreateTodoSubmit();
const submitTodo = onSubmit ?? handleSubmit;

const handleAddClick = () => {
overlay.open(({ isOpen, close, unmount }) => (
Expand All @@ -27,14 +34,18 @@ export const CreateTodoModalContainer = ({
onClose={close}
onExited={unmount}
defaultDate={defaultDate}
onSubmit={handleSubmit}
onSubmit={submitTodo}
/>
));
};

return (
<>
<AddTaskButton text={t("addTask")} onClick={handleAddClick} />
<AddTaskButton
variant={buttonVariant}
text={t("addTask")}
onClick={handleAddClick}
/>

<AnimatedToast
isOpen={isErrorToastOpen}
Expand Down
Loading
Loading