Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
7537042
chore(web): 투두 모달 관련 의존성 추가 (#137)
kimminna Jul 10, 2026
ab714de
feat(ui): 투두 생성 모달용 디자인시스템 컴포넌트 추가 (#137)
kimminna Jul 10, 2026
b5c3cb9
feat(web): 투두 API 스키마 및 오버레이 모달 인프라 추가 (#137)
kimminna Jul 10, 2026
87c8f56
feat(web): 홈 투두 생성 모달 및 투두 카드 기능 구현 (#137)
kimminna Jul 10, 2026
48a8cc6
name(web): 홈 훅 파일명을 kebab-case로 변경 (#137)
kimminna Jul 10, 2026
97f30e1
refactor(web): 투두 생성 모달 컨테이너 책임 분리 (#137)
kimminna Jul 10, 2026
692ddbf
Merge branch 'develop' of https://github.com/Team-Timo/Timo-client in…
kimminna Jul 11, 2026
6813cc5
fix(ui): TagNameInput 키보드 접근성 개선 (#137)
kimminna Jul 11, 2026
1650c48
fix(web): TagNameInput에 접근성 라벨 전달 (#137)
kimminna Jul 11, 2026
d2d4a12
refactor(web): 날짜 관련 유틸을 공통 폴더로 통합
kimminna Jul 11, 2026
424172e
fix(web): 투두 우선순위 매핑값을 디자인시스템 Priority 타입에 맞게 수정
kimminna Jul 11, 2026
3601e91
refactor(ui): TodoIconValue를 공용 상수 TODO_ICON_VALUES에서 파생하도록 정리 (#137)
kimminna Jul 11, 2026
e6019fe
refactor(web): todoIconSchema가 디자인시스템 TODO_ICON_VALUES를 참조하도록 변경 (#137)
kimminna Jul 11, 2026
d004c27
chore(web): focus-trap-react 의존성 추가 (#137)
kimminna Jul 11, 2026
c60c9db
fix(web): Modal을 OverlayModal로 이름 변경하고 접근성 개선 (#137)
kimminna Jul 11, 2026
f5cd9f1
fix(web): OverlayModal 배경 클릭 시 닫히지 않는 문제 수정 (#137)
kimminna Jul 11, 2026
2d77c64
fix(web): OverlayModal에 필수 접근 가능한 이름 적용 (#137)
kimminna Jul 11, 2026
ce97f90
fix(web): OverlayModal 포커스 트랩이 바깥 클릭 시 단독으로 해제되는 문제 수정 (#137)
kimminna Jul 12, 2026
844114a
fix(web): 투두 생성 폼 defaultDate 리셋 버그 수정 및 닫기 버튼 aria-label 통합 (#137)
kimminna Jul 12, 2026
683286c
fix(web): subtask 생성 시 ID 충돌 가능성 방지 (#137)
kimminna Jul 12, 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
83 changes: 83 additions & 0 deletions apps/timo-web/api/todo/todo-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { TODO_ICON_VALUES } from "@repo/timo-design-system/ui";
import { z } from "zod";

import { apiResponseSchema } from "@/api/schema/response";

export const todoIconSchema = z.enum(TODO_ICON_VALUES);

export const todoPrioritySchema = z.enum([
"VERY_HIGH",
"HIGH",
"MEDIUM",
"LOW",
]);

export const todoRepeatTypeSchema = z.enum([
"NONE",
"DAILY",
"WEEKLY",
"MONTHLY",
]);

export const todoRepeatWeekdaySchema = z.enum([
"MON",
"TUE",
"WED",
"THU",
"FRI",
"SAT",
"SUN",
]);

const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
const DURATION_PATTERN = /^\d{1,3}:[0-5]\d$/;

export const createTodoRequestSchema = z
.object({
icon: todoIconSchema.nullable(),
title: z.string().trim().min(1),
subtasks: z.array(z.string().trim().min(1)).nullable(),
date: z.string().regex(DATE_PATTERN),
duration: z.string().regex(DURATION_PATTERN),
priority: todoPrioritySchema.nullable(),
tagId: z.number().int().positive().nullable(),
repeatType: todoRepeatTypeSchema,
repeatWeekdays: z.array(todoRepeatWeekdaySchema).nullable(),
repeatDayOfMonth: z.number().int().min(1).max(31).nullable(),
memo: z.string().nullable(),
})
.superRefine((value, ctx) => {
if (
value.repeatType === "WEEKLY" &&
(!value.repeatWeekdays || value.repeatWeekdays.length === 0)
) {
ctx.addIssue({
code: "custom",
path: ["repeatWeekdays"],
message: "반복할 요일을 선택해주세요.",
});
}

if (value.repeatType === "MONTHLY" && !value.repeatDayOfMonth) {
ctx.addIssue({
code: "custom",
path: ["repeatDayOfMonth"],
message: "반복할 날짜를 선택해주세요.",
});
}
});
Comment thread
kimminna marked this conversation as resolved.

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

export type TodoIcon = z.infer<typeof todoIconSchema>;
export type TodoPriority = z.infer<typeof todoPrioritySchema>;
export type TodoRepeatType = z.infer<typeof todoRepeatTypeSchema>;
export type TodoRepeatWeekday = z.infer<typeof todoRepeatWeekdaySchema>;

export const createTodoResponseSchema = apiResponseSchema(
z.object({
todoId: z.number(),
}),
);

export type CreateTodoResponse = z.infer<typeof createTodoResponseSchema>;
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,10 @@ import type {

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

const PRIORITY_MAP: Record<
TodoPriorityTypes,
"urgent" | "high" | "medium" | "low"
> = {
URGENT: "urgent",
type PriorityLabelKeyTypes = "urgent" | "high" | "medium" | "low";

const PRIORITY_LABEL_KEY: Record<TodoPriorityTypes, PriorityLabelKeyTypes> = {
VERY_HIGH: "urgent",
HIGH: "high",
MEDIUM: "medium",
LOW: "low",
Expand Down Expand Up @@ -76,12 +75,11 @@ export const HomeTodoCard = ({
const sortableStyle = {
transform: CSS.Transform.toString(transform),
transition,
touchAction: "none",
};

const isRunning = timerStatus === "RUNNING";

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

const titleRow = (
<div className="flex w-full items-center justify-between gap-2">
Expand Down Expand Up @@ -148,7 +146,7 @@ export const HomeTodoCard = ({
<div className="flex w-full items-center justify-between gap-2">
<div className="flex min-w-0 items-center gap-1">
<PriorityIcon
priority={isCompleted ? "Disable" : PRIORITY_MAP[priority]}
priority={isCompleted ? "Disable" : priority}
label={priorityLabel}
/>
{tagName && <TagIcon text={tagName} />}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { ChevronUpIcon } from "@repo/timo-design-system/icons";
import { IconGraphic, IconSelector } from "@repo/timo-design-system/ui";
import { cn } from "@repo/timo-design-system/utils";

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

export interface CreateTodoIconFieldProps {
icon: TodoIconValue | null;
isIconPanelOpen: boolean;
addIconLabel: string;
onOpenPanel: () => void;
onTogglePanel: () => void;
onSelectIcon: (icon: TodoIconValue) => void;
onRemoveIcon: () => void;
}

export const CreateTodoIconField = ({
icon,
isIconPanelOpen,
addIconLabel,
onOpenPanel,
onTogglePanel,
onSelectIcon,
onRemoveIcon,
}: CreateTodoIconFieldProps) => {
return (
<div className="flex w-full flex-col items-start gap-2">
{!isIconPanelOpen && icon ? (
<button
type="button"
onClick={onOpenPanel}
aria-label={addIconLabel}
className="flex size-6 shrink-0 items-center justify-center rounded-[4px]"
>
<IconGraphic icon={icon} />
</button>
) : (
<button
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"
>
<ChevronUpIcon
width={18}
height={18}
className={cn(
"shrink-0 transition-transform duration-200 ease-in-out",
!isIconPanelOpen && "rotate-180",
)}
/>
<span className="typo-caption-r-10 text-timo-gray-700 whitespace-nowrap">
{addIconLabel}
</span>
</button>
)}

{isIconPanelOpen && (
<IconSelector
selected={icon}
onSelect={onSelectIcon}
onRemove={onRemoveIcon}
/>
)}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export interface CreateTodoMemoFieldProps {
value: string;
placeholder: string;
onChange: (value: string) => void;
}

export const CreateTodoMemoField = ({
value,
placeholder,
onChange,
}: CreateTodoMemoFieldProps) => {
return (
<textarea
value={value}
onChange={(event) => onChange(event.target.value)}
placeholder={placeholder}
rows={1}
className="typo-body-r-12 text-timo-gray-800 w-full resize-none outline-none"
/>
);
Comment thread
kimminna marked this conversation as resolved.
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Checkbox } from "@repo/timo-design-system/ui";

import type { CreateTodoRequest } from "@/api/todo/todo-schema";
import type { UseFormRegister } from "react-hook-form";

export interface CreateTodoTaskFieldsProps {
register: UseFormRegister<CreateTodoRequest>;
titlePlaceholder: string;
subtaskInput: string;
subtaskPlaceholder: string;
onSubtaskInputChange: (value: string) => void;
}

export const CreateTodoTaskFields = ({
register,
titlePlaceholder,
subtaskInput,
subtaskPlaceholder,
onSubtaskInputChange,
}: CreateTodoTaskFieldsProps) => {
return (
<div className="flex w-full flex-col items-start gap-1.5">
<div className="flex w-full items-center gap-2">
<Checkbox checked={false} onChange={() => {}} disabled />
<input
{...register("title")}
placeholder={titlePlaceholder}
className="typo-headline-b-14 text-timo-black min-w-0 flex-1 outline-none"
/>
</div>
<div className="flex w-full items-center gap-2">
<Checkbox checked={false} onChange={() => {}} disabled />
<input
value={subtaskInput}
onChange={(event) => onSubtaskInputChange(event.target.value)}
placeholder={subtaskPlaceholder}
className="typo-body-r-12 text-timo-gray-800 min-w-0 flex-1 outline-none"
/>
</div>
Comment thread
kimminna marked this conversation as resolved.
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

import { useTranslations } from "next-intl";

import { triggerScrollToToday } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/useHomeTodayScroll";
import { useHomeViewMode } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/useHomeViewMode";
import { triggerScrollToToday } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/use-home-today-scroll";
import { useHomeViewMode } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/use-home-view-mode";
import { Header } from "@/components/layout/header/Header";
import { useNavigationSidebar } from "@/components/layout/sidebar/navigation/NavigationSidebarContext";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ import { useEffect, useMemo } 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/HomeTodoCard";
import { HomeDayHeaderContainer } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeDayHeaderContainer";
import { useHomeTodayScrollRef } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/useHomeTodayScroll";
import { useHomeTodosByDate } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/useHomeTodosByDate";
import { useHomeViewMode } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/useHomeViewMode";
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 { 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 { formatDateKey } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_utils/date";
import { reorderDaysTodayFirst } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_utils/home-view";
import { DndSortableListProvider } from "@/providers/dnd/DndSortableListProvider";
import { formatDateKey } from "@/utils/date";

const TAG_LABEL_KEYS = [
"dailyLife",
Expand Down Expand Up @@ -50,6 +50,7 @@ export const HomeTodoContainer = () => {

const {
todosByDate,
handleAddTodo,
handleToggleCompleted,
handleTogglePlay,
handleToggleSubtaskCompleted,
Expand Down Expand Up @@ -90,6 +91,7 @@ export const HomeTodoContainer = () => {
isToday={day.isToday}
totalCount={todos.length}
completedCount={completedCount}
onCreateTodo={(todo) => handleAddTodo(dateKey, todo)}
/>

<DndSortableListProvider
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"use client";

import { DeleteIcon } from "@repo/timo-design-system/icons";
import { CreateButton, TagNameInput } from "@repo/timo-design-system/ui";
import { useTranslations } from "next-intl";
import { useState } from "react";

import { OverlayModal } from "@/components/modal/OverlayModal";

const MAX_TAG_NAME_LENGTH = 10;

export interface CreateTagModalContainerProps {
isOpen: boolean;
onClose: () => void;
onExited: () => void;
existingLabels: string[];
onCreate: (label: string) => void;
}

export const CreateTagModalContainer = ({
isOpen,
onClose,
onExited,
existingLabels,
onCreate,
}: CreateTagModalContainerProps) => {
const t = useTranslations("Home");
const tCommon = useTranslations("Common");
const [name, setName] = useState<string>("");

const trimmedName = name.trim();
const isDuplicate = existingLabels.includes(trimmedName);
const isValid =
trimmedName.length > 0 &&
trimmedName.length <= MAX_TAG_NAME_LENGTH &&
!isDuplicate;

const handleCreate = () => {
if (!isValid) return;

onCreate(trimmedName);
setName("");
};

return (
<OverlayModal
isOpen={isOpen}
onClose={onClose}
onExited={onExited}
ariaLabel={t("createTagModal.title")}
className="w-[490px] items-center gap-3 px-6 py-4"
>
<div className="flex w-full items-center justify-between">
<p className="typo-body-sb-12 text-timo-blue-300">
{t("createTagModal.title")}
</p>
<button
type="button"
aria-label={tCommon("close")}
onClick={onClose}
className="shrink-0"
>
<DeleteIcon />
</button>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</div>

<div className="flex w-full flex-col items-start gap-3">
<p className="typo-headline-m-16 text-timo-black w-full">
{t("createTagModal.nameLabel")}
</p>

<TagNameInput
value={name}
onChange={setName}
maxLength={MAX_TAG_NAME_LENGTH}
isError={isDuplicate}
ariaLabel={t("createTagModal.nameLabel")}
maxLengthHint={t("createTagModal.maxLengthHint")}
duplicateHint={t("createTagModal.duplicateHint")}
/>
</div>

<div className="flex w-full items-center justify-end">
<CreateButton
label={t("createTagModal.create")}
disabled={!isValid}
onClick={handleCreate}
/>
</div>
</OverlayModal>
);
};
Loading
Loading