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
7 changes: 7 additions & 0 deletions apps/timo-web/api/todo/todo-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,14 @@ export const createTodoRequestSchema = z
}
});

export const recommendDurationResponseSchema = z.object({
recommendedMinutes: z.number(),
});
Comment thread
kimminna marked this conversation as resolved.

export type CreateTodoRequest = z.infer<typeof createTodoRequestSchema>;
export type RecommendDurationResponseData = z.infer<
typeof recommendDurationResponseSchema
>;

export type TodoIcon = z.infer<typeof todoIconSchema>;
export type TodoPriority = z.infer<typeof todoPrioritySchema>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@

import { cn } from "@repo/timo-design-system/utils";
import { useTranslations } from "next-intl";
import { useEffect, useMemo } from "react";
import { useEffect } from "react";

import type { HomeViewFilter } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_types/home-view-type";

import { HomeTodoCard } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_components/todo-card/HomeTodoCard";
import { HomeDayHeaderContainer } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_containers/todo-card/HomeDayHeaderContainer";
import { useHomeTodayScrollRef } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/use-home-today-scroll";
import {
scrollContainerToToday,
useHomeTodayScrollRef,
} from "@/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/use-home-today-scroll";
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 { getHomeViewMock } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_mocks/home-view-mock";
import { reorderDaysTodayFirst } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_utils/home-view";
import { useHomeView } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_queries/use-home-view";
import { DndSortableListProvider } from "@/providers/dnd/DndSortableListProvider";
import { formatDateKey } from "@/utils/date";

Expand All @@ -38,27 +40,19 @@ export const HomeTodoContainer = () => {
const filter: HomeViewFilter = isWeekView ? "WEEK" : "DEFAULT";
const baseDate = formatDateKey(referenceDate);

const apiDays = useMemo(
() => getHomeViewMock({ filter, baseDate }).days,
[filter, baseDate],
);

const days = useMemo(
() => (isWeekView ? apiDays : reorderDaysTodayFirst(apiDays)),
[isWeekView, apiDays],
);
const { data: homeViewData } = useHomeView({ filter, baseDate });
const days = homeViewData.days;

const {
todosByDate,
handleAddTodo,
handleToggleCompleted,
handleTogglePlay,
handleToggleSubtaskCompleted,
handleReorderTodo,
} = useHomeTodosByDate(apiDays);
} = useHomeTodosByDate(days);

useEffect(() => {
scrollRef.current?.scrollTo({ left: 0 });
scrollContainerToToday(scrollRef.current);
}, [isWeekView, scrollRef]);
Comment thread
kimminna marked this conversation as resolved.

return (
Expand All @@ -77,6 +71,7 @@ export const HomeTodoContainer = () => {
return (
<div
key={dateKey}
data-today={day.isToday}
className={cn(
"flex h-full flex-col gap-2",
isWeekView
Expand All @@ -91,7 +86,6 @@ export const HomeTodoContainer = () => {
isToday={day.isToday}
totalCount={todos.length}
completedCount={completedCount}
onCreateTodo={(todo) => handleAddTodo(dateKey, todo)}
/>

<DndSortableListProvider
Expand All @@ -114,9 +108,10 @@ export const HomeTodoContainer = () => {
durationSeconds={todo.durationSeconds}
priority={todo.priority}
tagName={
isTagLabelKey(todo.tag.name)
todo.tag &&
(isTagLabelKey(todo.tag.name)
? tCommon(`tag.${todo.tag.name}`)
: todo.tag.name
: todo.tag.name)
}
hasMemo={todo.hasMemo}
isRepeated={todo.isRepeated}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import { useTranslations } from "next-intl";

import type { ApiDayOfWeek } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_types/home-view-type";
import type { Todo } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_types/todo-type";

import { HomeDateInformation } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_components/todo-card/HomeDateInformation";
import { CreateTodoModalContainer } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_containers/todo-modal/CreateTodoModalContainer";
Expand All @@ -17,7 +16,6 @@ export interface HomeDayHeaderContainerProps {
isToday: boolean;
totalCount: number;
completedCount: number;
onCreateTodo: (todo: Todo) => void;
}

export const HomeDayHeaderContainer = ({
Expand All @@ -27,7 +25,6 @@ export const HomeDayHeaderContainer = ({
isToday,
totalCount,
completedCount,
onCreateTodo,
}: HomeDayHeaderContainerProps) => {
const tCommon = useTranslations("Common");
const date = parseDateKey(dateKey) ?? getToday();
Expand All @@ -42,7 +39,7 @@ export const HomeDayHeaderContainer = ({
totalCount={totalCount}
completedCount={completedCount}
/>
<CreateTodoModalContainer defaultDate={date} onCreate={onCreateTodo} />
<CreateTodoModalContainer defaultDate={date} />
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,21 @@ import { AddTaskButton } from "@repo/timo-design-system/ui";
import { useTranslations } from "next-intl";
import { overlay } from "overlay-kit";

import type { Todo } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_types/todo-type";

import { CreateTodoModalContent } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_containers/todo-modal/CreateTodoModalContent";
import { useCreateTodoSubmit } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/todo-modal/use-create-todo-submit";
import { AnimatedToast } from "@/components/toast/AnimatedToast";

export interface CreateTodoModalContainerProps {
defaultDate?: Date;
onCreate: (todo: Todo) => void;
}

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

const handleAddClick = () => {
overlay.open(({ isOpen, close, unmount }) => (
Expand All @@ -33,5 +32,15 @@ export const CreateTodoModalContainer = ({
));
};

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

<AnimatedToast
isOpen={isErrorToastOpen}
onClose={closeErrorToast}
message={tToast("todoCreateFailed")}
/>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,19 @@ import { CreateTodoTaskFields } from "@/app/[locale]/(main)/(with-time-sidebar)/
import { useIconField } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/todo-modal/use-icon-field";
import { useRepeatField } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/todo-modal/use-repeat-field";
import { useSubtaskField } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/todo-modal/use-subtask-field";
import {
DEFAULT_TAG,
useTagField,
} from "@/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/todo-modal/use-tag-field";
import { useTagField } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/todo-modal/use-tag-field";
import { useTimeField } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/todo-modal/use-time-field";
import { useTitleField } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/todo-modal/use-title-field";
import { formatDateToIsoDate } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_utils/date";
import { OverlayModal } from "@/components/modal/OverlayModal";
import { AnimatedToast } from "@/components/toast/AnimatedToast";

export interface CreateTodoTag {
id: number;
name: string;
}

const createDefaultValues = (defaultDate?: Date): CreateTodoRequest => ({
icon: null,
title: "",
subtasks: [],
date: formatDateToIsoDate(defaultDate ?? new Date()),
duration: "0:00",
duration: "00:00",
priority: null,
tagId: null,
repeatType: "NONE",
Expand All @@ -50,7 +42,7 @@ export interface CreateTodoModalContentProps {
onClose: () => void;
onExited: () => void;
defaultDate?: Date;
onSubmit: (data: CreateTodoRequest, tag: CreateTodoTag) => void;
onSubmit: (data: CreateTodoRequest) => void;
}

export const CreateTodoModalContent = ({
Expand Down Expand Up @@ -88,9 +80,7 @@ export const CreateTodoModalContent = ({
: undefined;

const handleFormSubmit = (data: CreateTodoRequest) => {
const tag = tagField.selectedTagOption ?? DEFAULT_TAG;

onSubmit(data, { id: data.tagId ?? tag.id, name: tag.name });
onSubmit(data);

reset(createDefaultValues(defaultDate));
subtaskField.reset();
Expand Down Expand Up @@ -169,6 +159,7 @@ export const CreateTodoModalContent = ({
onTimeChange={timeField.handleDurationInputChange}
selectedTime={timeField.selectedTime}
onSelectTime={timeField.handleSelectTime}
onTimeOpen={timeField.handleTimeSelectorOpen}
priority={priorityField.value ?? undefined}
onSelectPriority={(level: PriorityLevel) =>
priorityField.onChange(level)
Expand Down Expand Up @@ -228,6 +219,12 @@ export const CreateTodoModalContent = ({
</p>
}
/>

<AnimatedToast
isOpen={timeField.isAiDurationErrorToastOpen}
onClose={timeField.closeAiDurationErrorToast}
message={tToast("aiDurationRecommendFailed")}
/>
</>
);
};
Original file line number Diff line number Diff line change
@@ -1,42 +1,60 @@
"use client";

import { useQueryClient } from "@tanstack/react-query";
import { useState } from "react";

import type { TodoCreateRequest } from "@/api/generated/models";
import type { CreateTodoRequest } from "@/api/todo/todo-schema";
import type { CreateTodoTag } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_containers/todo-modal/CreateTodoModalContent";
import type { Todo } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_types/todo-type";

import { convertApiDurationToSeconds } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_utils/todo-time";
import { getGetHomeQueryKey } from "@/api/generated/endpoints/home/home";
import { useCreateTodo } from "@/api/generated/endpoints/todo/todo";
import { todoCreateResponseSchema } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_types/todo-type";

// 목데이터: 실제 API 호출 없이 로컬 상태에만 즉시 반영한다.
const buildTodoFromRequest = (
const buildCreateTodoRequestBody = (
data: CreateTodoRequest,
tag: CreateTodoTag,
): Todo => ({
todoId: Date.now(),
icon: data.icon,
): TodoCreateRequest => ({
icon: data.icon ?? undefined,
title: data.title,
completed: false,
durationSeconds: convertApiDurationToSeconds(data.duration),
priority: data.priority ?? "MEDIUM",
tag: { tagId: tag.id, name: tag.name },
hasMemo: Boolean(data.memo?.trim()),
isRepeated: data.repeatType !== "NONE",
timerStatus: "STOPPED",
sortOrder: 0,
subtasks: (data.subtasks ?? []).map((content, index) => ({
subtaskId: Date.now() + index,
content,
completed: false,
})),
subtasks: data.subtasks?.length ? data.subtasks : undefined,
date: data.date,
duration: data.duration,
priority: data.priority ?? undefined,
tagId: data.tagId ?? undefined,
repeatType: data.repeatType,
repeatWeekdays: data.repeatWeekdays?.length ? data.repeatWeekdays : undefined,
repeatDayOfMonth: data.repeatDayOfMonth ?? undefined,
memo: data.memo?.trim() ? data.memo : undefined,
});

export interface UseCreateTodoSubmitParams {
onCreate: (todo: Todo) => void;
}
export const useCreateTodoSubmit = () => {
const [isErrorToastOpen, setIsErrorToastOpen] = useState(false);
const { mutate: createTodo } = useCreateTodo();
const queryClient = useQueryClient();

const handleSubmit = (data: CreateTodoRequest) => {
createTodo(
{ data: buildCreateTodoRequestBody(data) },
{
onSuccess: (response) => {
const parsed = todoCreateResponseSchema.safeParse(response.data);

export const useCreateTodoSubmit = ({
onCreate,
}: UseCreateTodoSubmitParams) => {
const handleSubmit = (data: CreateTodoRequest, tag: CreateTodoTag) => {
onCreate(buildTodoFromRequest(data, tag));
if (!parsed.success) {
setIsErrorToastOpen(true);
return;
}

queryClient.invalidateQueries({ queryKey: getGetHomeQueryKey() });
},
onError: () => {
setIsErrorToastOpen(true);
},
},
);
};

return { handleSubmit };
return {
handleSubmit,
isErrorToastOpen,
closeErrorToast: () => setIsErrorToastOpen(false),
};
Comment thread
kimminna marked this conversation as resolved.
};
Loading
Loading