diff --git a/apps/timo-web/api/generated/endpoints/home/home.zod.ts b/apps/timo-web/api/generated/endpoints/home/home.zod.ts index 03dbf623..35356243 100644 --- a/apps/timo-web/api/generated/endpoints/home/home.zod.ts +++ b/apps/timo-web/api/generated/endpoints/home/home.zod.ts @@ -58,7 +58,7 @@ export const GetHomeResponse = zod.object({ name: zod.string(), }) .optional(), - hasMemo: zod.boolean(), + hasSubtask: zod.boolean(), isRepeated: zod.boolean(), timerStatus: zod.enum(["STOPPED", "RUNNING", "PAUSED"]), sortOrder: zod.number().optional(), @@ -107,7 +107,7 @@ export const GetTodayResponse = zod.object({ }) .optional(), isRepeated: zod.boolean(), - hasMemo: zod.boolean(), + hasSubtask: zod.boolean(), timerStatus: zod.enum(["STOPPED", "RUNNING", "PAUSED"]), sortOrder: zod.number().optional(), subtasks: zod.array( diff --git a/apps/timo-web/api/generated/models/todayTodoResponse.ts b/apps/timo-web/api/generated/models/todayTodoResponse.ts index b81b03b1..416cb136 100644 --- a/apps/timo-web/api/generated/models/todayTodoResponse.ts +++ b/apps/timo-web/api/generated/models/todayTodoResponse.ts @@ -19,7 +19,7 @@ export interface TodayTodoResponse { priority?: string; tag?: TagResponse; isRepeated: boolean; - hasMemo: boolean; + hasSubtask: boolean; timerStatus: TodayTodoResponseTimerStatus; sortOrder?: number; subtasks: SubtaskResponse[]; diff --git a/apps/timo-web/api/generated/models/todoCreateRequestIcon.ts b/apps/timo-web/api/generated/models/todoCreateRequestIcon.ts index 03aee0eb..68fad3e3 100644 --- a/apps/timo-web/api/generated/models/todoCreateRequestIcon.ts +++ b/apps/timo-web/api/generated/models/todoCreateRequestIcon.ts @@ -10,6 +10,7 @@ export type TodoCreateRequestIcon = (typeof TodoCreateRequestIcon)[keyof typeof TodoCreateRequestIcon]; export const TodoCreateRequestIcon = { + NONE: "NONE", ICON_1: "ICON_1", ICON_2: "ICON_2", ICON_3: "ICON_3", diff --git a/apps/timo-web/api/generated/models/todoResponse.ts b/apps/timo-web/api/generated/models/todoResponse.ts index 4992e5f1..a0ccd634 100644 --- a/apps/timo-web/api/generated/models/todoResponse.ts +++ b/apps/timo-web/api/generated/models/todoResponse.ts @@ -17,7 +17,7 @@ export interface TodoResponse { durationSeconds?: number; priority?: string; tag?: TagResponse; - hasMemo: boolean; + hasSubtask: boolean; isRepeated: boolean; timerStatus: TodoResponseTimerStatus; sortOrder?: number; diff --git a/apps/timo-web/api/generated/models/todoUpdateRequestIcon.ts b/apps/timo-web/api/generated/models/todoUpdateRequestIcon.ts index c290f59b..f23a7a76 100644 --- a/apps/timo-web/api/generated/models/todoUpdateRequestIcon.ts +++ b/apps/timo-web/api/generated/models/todoUpdateRequestIcon.ts @@ -10,6 +10,7 @@ export type TodoUpdateRequestIcon = (typeof TodoUpdateRequestIcon)[keyof typeof TodoUpdateRequestIcon]; export const TodoUpdateRequestIcon = { + NONE: "NONE", ICON_1: "ICON_1", ICON_2: "ICON_2", ICON_3: "ICON_3", diff --git a/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_components/todo-card/HomeTodoCard.tsx b/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_components/todo-card/HomeTodoCard.tsx index 3e98a59b..fad902ee 100644 --- a/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_components/todo-card/HomeTodoCard.tsx +++ b/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_components/todo-card/HomeTodoCard.tsx @@ -17,6 +17,7 @@ import { } from "@repo/timo-design-system/ui"; import { cn } from "@repo/timo-design-system/utils"; import { useTranslations } from "next-intl"; +import { useEffect, useState } from "react"; import type { TodoPriorityTypes, @@ -26,15 +27,6 @@ import type { KeyboardEvent, MouseEvent, PointerEvent } from "react"; import { convertDurationToTimeText } from "@/utils/todo/todo-time"; -type PriorityLabelKeyTypes = "urgent" | "high" | "medium" | "low"; - -const PRIORITY_LABEL_KEY: Record = { - VERY_HIGH: "urgent", - HIGH: "high", - MEDIUM: "medium", - LOW: "low", -}; - const isInteractiveElement = (target: EventTarget | null) => target instanceof HTMLElement && Boolean(target.closest("button, input, label")); @@ -44,9 +36,9 @@ export interface HomeTodoCardProps { title: string; isCompleted: boolean; durationSeconds: number; - priority: TodoPriorityTypes; + priority?: TodoPriorityTypes; tagName?: string; - hasMemo: boolean; + hasSubtask: boolean; isRepeated: boolean; timerStatus: TodoTimerStatusTypes; isPlayHighlighted: boolean; @@ -65,7 +57,7 @@ export const HomeTodoCard = ({ durationSeconds, priority, tagName, - hasMemo, + hasSubtask, isRepeated, timerStatus, isPlayHighlighted, @@ -86,9 +78,16 @@ export const HomeTodoCard = ({ transition, }; + const [isHovered, setIsHovered] = useState(false); + + useEffect(() => { + if (isCompleted) setIsHovered(false); + }, [isCompleted]); + + const isDimmed = isCompleted && !isHovered; const isRunning = timerStatus === "RUNNING"; - const priorityLabel = tCommon(`priority.${PRIORITY_LABEL_KEY[priority]}`); + const priorityLabel = priority ? tCommon(`priority.${priority}`) : undefined; const handleCardClick = (event: MouseEvent) => { if (isInteractiveElement(event.target)) return; @@ -123,7 +122,7 @@ export const HomeTodoCard = ({

{title} @@ -165,9 +164,11 @@ export const HomeTodoCard = ({ tabIndex={onClickTodo ? 0 : attributes.tabIndex} onClick={onClickTodo ? handleCardClick : undefined} onKeyDown={onClickTodo ? handleCardKeyDown : undefined} + onMouseEnter={() => setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} 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", + isDimmed ? "bg-timo-gray-200" : "bg-white", onClickTodo && "cursor-pointer", )} > @@ -189,19 +190,23 @@ export const HomeTodoCard = ({ )}

- + {priority && ( +
+ +
+ )} {tagName && } - {hasMemo && - (isCompleted ? ( + {hasSubtask && + (isDimmed ? ( ) : ( ))} {isRepeated && - (isCompleted ? ( + (isDimmed ? ( ) : ( @@ -210,7 +215,7 @@ export const HomeTodoCard = ({ {convertDurationToTimeText(durationSeconds)} diff --git a/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsx b/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsx index e0aae773..aa7064f9 100644 --- a/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsx +++ b/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsx @@ -174,7 +174,7 @@ export const HomeTodoContainer = () => { durationSeconds={durationSeconds} priority={todo.priority} tagName={todoTagName} - hasMemo={todo.hasMemo} + hasSubtask={todo.hasSubtask} isRepeated={todo.isRepeated} timerStatus={timerStatus} isPlayHighlighted={isPlayHighlighted} diff --git a/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_mocks/todo-mock.ts b/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_mocks/todo-mock.ts index 9e3abf30..73708040 100644 --- a/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_mocks/todo-mock.ts +++ b/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_mocks/todo-mock.ts @@ -15,7 +15,7 @@ const TODO_TEMPLATES: TodoTemplate[] = [ durationSeconds: 7200, priority: "HIGH", tag: { tagId: 3, name: "work" }, - hasMemo: false, + hasSubtask: false, isRepeated: true, timerStatus: "RUNNING", sortOrder: 0, @@ -28,7 +28,7 @@ const TODO_TEMPLATES: TodoTemplate[] = [ durationSeconds: 5400, priority: "VERY_HIGH", tag: { tagId: 1, name: "assignment" }, - hasMemo: true, + hasSubtask: true, isRepeated: false, timerStatus: "STOPPED", sortOrder: 1, @@ -47,7 +47,7 @@ const TODO_TEMPLATES: TodoTemplate[] = [ durationSeconds: 1800, priority: "LOW", tag: { tagId: 4, name: "exercise" }, - hasMemo: false, + hasSubtask: false, isRepeated: true, timerStatus: "STOPPED", sortOrder: 2, @@ -60,7 +60,7 @@ const TODO_TEMPLATES: TodoTemplate[] = [ durationSeconds: 1800, priority: "MEDIUM", tag: { tagId: 5, name: "additional" }, - hasMemo: true, + hasSubtask: true, isRepeated: false, timerStatus: "STOPPED", sortOrder: 3, @@ -73,7 +73,7 @@ const TODO_TEMPLATES: TodoTemplate[] = [ durationSeconds: 3600, priority: "MEDIUM", tag: { tagId: 2, name: "dailyLife" }, - hasMemo: false, + hasSubtask: false, isRepeated: false, timerStatus: "STOPPED", sortOrder: 4, diff --git a/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_components/TodayTodoCard.tsx b/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_components/TodayTodoCard.tsx index 52ae7af6..796d9ba5 100644 --- a/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_components/TodayTodoCard.tsx +++ b/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_components/TodayTodoCard.tsx @@ -40,7 +40,7 @@ export interface TodayTodoCardToolbar { time: string; priority?: PriorityLevel; tag?: string; - hasMemo: boolean; + hasSubtask: boolean; hasRepeat: boolean; } @@ -185,7 +185,7 @@ export const TodayTodoCard = ({ tagLabel={toolbar.tag ?? "태그"} tags={[]} selectedTag={toolbar.tag} - hasMemo={toolbar.hasMemo} + hasMemo={toolbar.hasSubtask} isRepeatActive={toolbar.hasRepeat} repeat={{ detailHeading: "상세 설정", diff --git a/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayTodoListContainer.tsx b/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayTodoListContainer.tsx index 63646197..1cd913af 100644 --- a/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayTodoListContainer.tsx +++ b/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayTodoListContainer.tsx @@ -121,7 +121,7 @@ export const TodayTodoListContainer = () => { time: convertDurationToTimeText(durationSeconds), priority: todo.priority, tag: todo.tag?.name, - hasMemo: todo.hasMemo, + hasSubtask: todo.hasSubtask, hasRepeat: todo.isRepeated, }} subTodos={todo.subtasks.map((s) => ({ diff --git a/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_mocks/today-todo-mock.ts b/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_mocks/today-todo-mock.ts index 41ebd086..c7344633 100644 --- a/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_mocks/today-todo-mock.ts +++ b/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_mocks/today-todo-mock.ts @@ -18,7 +18,7 @@ export interface TodoMock { durationSeconds: number; priority: "URGENT" | "HIGH" | "MEDIUM" | "LOW"; tag: TodoTag | null; - hasMemo: boolean; + hasSubtask: boolean; isRepeated: boolean; timerStatus: "RUNNING" | "PAUSED" | "STOPPED"; sortOrder: number; @@ -35,7 +35,7 @@ export const todayTodoMocks: TodoMock[] = [ durationSeconds: 36000, priority: "URGENT", tag: { tagId: 1, name: "작업" }, - hasMemo: true, + hasSubtask: true, isRepeated: false, timerStatus: "STOPPED", sortOrder: 0, @@ -53,7 +53,7 @@ export const todayTodoMocks: TodoMock[] = [ durationSeconds: 3600, priority: "MEDIUM", tag: { tagId: 2, name: "완료" }, - hasMemo: false, + hasSubtask: false, isRepeated: false, timerStatus: "STOPPED", sortOrder: 1, @@ -68,7 +68,7 @@ export const todayTodoMocks: TodoMock[] = [ durationSeconds: 1800, priority: "HIGH", tag: null, - hasMemo: true, + hasSubtask: true, isRepeated: true, timerStatus: "STOPPED", sortOrder: 2, @@ -83,7 +83,7 @@ export const todayTodoMocks: TodoMock[] = [ durationSeconds: 7200, priority: "HIGH", tag: { tagId: 3, name: "개발" }, - hasMemo: false, + hasSubtask: false, isRepeated: false, timerStatus: "STOPPED", sortOrder: 3, @@ -101,7 +101,7 @@ export const todayTodoMocks: TodoMock[] = [ durationSeconds: 2700, priority: "MEDIUM", tag: { tagId: 4, name: "회의" }, - hasMemo: true, + hasSubtask: true, isRepeated: true, timerStatus: "STOPPED", sortOrder: 4, @@ -116,7 +116,7 @@ export const todayTodoMocks: TodoMock[] = [ durationSeconds: 5400, priority: "URGENT", tag: { tagId: 1, name: "작업" }, - hasMemo: false, + hasSubtask: false, isRepeated: false, timerStatus: "STOPPED", sortOrder: 5, @@ -131,7 +131,7 @@ export const todayTodoMocks: TodoMock[] = [ durationSeconds: 4500, priority: "LOW", tag: { tagId: 3, name: "개발" }, - hasMemo: false, + hasSubtask: false, isRepeated: false, timerStatus: "STOPPED", sortOrder: 6, @@ -149,7 +149,7 @@ export const todayTodoMocks: TodoMock[] = [ durationSeconds: 9000, priority: "HIGH", tag: { tagId: 3, name: "개발" }, - hasMemo: true, + hasSubtask: true, isRepeated: false, timerStatus: "STOPPED", sortOrder: 7, diff --git a/apps/timo-web/components/layout/sidebar/time/TimeboxPanel.tsx b/apps/timo-web/components/layout/sidebar/time/TimeboxPanel.tsx index da45c122..1d02adb3 100644 --- a/apps/timo-web/components/layout/sidebar/time/TimeboxPanel.tsx +++ b/apps/timo-web/components/layout/sidebar/time/TimeboxPanel.tsx @@ -89,7 +89,7 @@ export const TimeboxPanel = ({ currentTime }: TimeboxPanelProps) => { style={{ top: startOffset, height }} > {showLabel && ( -
+
{ + const [shouldRenderPanel, setShouldRenderPanel] = useState(isIconPanelOpen); + const [isPanelVisible, setIsPanelVisible] = useState(false); + + useEffect(() => { + if (isIconPanelOpen) { + setShouldRenderPanel(true); + const showFrame = requestAnimationFrame(() => setIsPanelVisible(true)); + return () => cancelAnimationFrame(showFrame); + } + + setIsPanelVisible(false); + const hideTimer = setTimeout( + () => setShouldRenderPanel(false), + PANEL_EXIT_ANIMATION_DURATION, + ); + return () => clearTimeout(hideTimer); + }, [isIconPanelOpen]); + return (
- {!isIconPanelOpen && icon ? ( + {!isIconPanelOpen && !shouldRenderPanel && icon ? ( @@ -39,7 +60,7 @@ export const TodoIconField = ({ type="button" onClick={onTogglePanel} aria-expanded={isIconPanelOpen} - className="bg-timo-gray-200 flex items-center gap-1 rounded-[4px] px-1.5 py-0.5" + className="bg-timo-gray-200 flex items-center gap-1 rounded-[4px] px-1.5 py-0.5 transition-opacity duration-150 ease-out" > )} - {isIconPanelOpen && ( - + {shouldRenderPanel && ( +
+ +
)}
); diff --git a/apps/timo-web/components/todo-modal/create/CreateTodoModalContent.tsx b/apps/timo-web/components/todo-modal/create/CreateTodoModalContent.tsx index fcd580a0..3847b648 100644 --- a/apps/timo-web/components/todo-modal/create/CreateTodoModalContent.tsx +++ b/apps/timo-web/components/todo-modal/create/CreateTodoModalContent.tsx @@ -128,6 +128,7 @@ export const CreateTodoModalContent = ({ titleValue={titleField.title} titlePlaceholder={t("createModal.titlePlaceholder")} onTitleChange={titleField.handleTitleChange} + onTitleEnter={subtaskField.focusFirstInput} subtaskInputs={subtaskField.subtaskInputs} subtaskPlaceholder={t("createModal.subtaskPlaceholder")} registerSubtaskInputRef={subtaskField.registerInputRef} @@ -162,10 +163,10 @@ export const CreateTodoModalContent = ({ onTimeOpen={timeField.handleTimeSelectorOpen} priority={priorityField.value ?? undefined} priorityLabels={{ - VERY_HIGH: tCommon("priority.urgent"), - HIGH: tCommon("priority.high"), - MEDIUM: tCommon("priority.medium"), - LOW: tCommon("priority.low"), + VERY_HIGH: tCommon("priority.VERY_HIGH"), + HIGH: tCommon("priority.HIGH"), + MEDIUM: tCommon("priority.MEDIUM"), + LOW: tCommon("priority.LOW"), }} onSelectPriority={(level: PriorityLevel) => priorityField.onChange(level) @@ -193,7 +194,7 @@ export const CreateTodoModalContent = ({ weekly: { weekdays: repeatField.weekdays, selectedWeekdayIds: repeatField.repeatWeekdays, - onWeekdayToggle: repeatField.handleWeekdayToggle, + onWeekdaysChange: repeatField.handleWeekdaysChange, }, monthly: { repeatDayLabel: t("createModal.repeatDayUnit"), diff --git a/apps/timo-web/components/todo-modal/create/CreateTodoTaskFields.tsx b/apps/timo-web/components/todo-modal/create/CreateTodoTaskFields.tsx index 4216658a..e63121df 100644 --- a/apps/timo-web/components/todo-modal/create/CreateTodoTaskFields.tsx +++ b/apps/timo-web/components/todo-modal/create/CreateTodoTaskFields.tsx @@ -1,4 +1,5 @@ import { Checkbox } from "@repo/timo-design-system/ui"; +import { useId } from "react"; import type { SubtaskInputEntry } from "@/utils/todo/subtask-input-list"; import type { KeyboardEvent } from "react"; @@ -13,6 +14,7 @@ export interface CreateTodoTaskFieldsProps { titleValue: string; titlePlaceholder: string; onTitleChange: (value: string) => void; + onTitleEnter: () => void; subtaskInputs: SubtaskInputEntry[]; subtaskPlaceholder: string; registerSubtaskInputRef: ( @@ -29,12 +31,15 @@ export const CreateTodoTaskFields = ({ titleValue, titlePlaceholder, onTitleChange, + onTitleEnter, subtaskInputs, subtaskPlaceholder, registerSubtaskInputRef, onSubtaskInputChange, onSubtaskInputKeyDown, }: CreateTodoTaskFieldsProps) => { + const titleInputId = useId(); + return (
@@ -43,16 +48,23 @@ export const CreateTodoTaskFields = ({ onChange={() => {}} className="cursor-default" /> -