Skip to content
Open
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
91 changes: 91 additions & 0 deletions apps/timo-web/api/todo/todo-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { z } from "zod";

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

export const todoIconSchema = z.enum([
"ICON_1",
"ICON_2",
"ICON_3",
"ICON_4",
"ICON_5",
"ICON_6",
"ICON_7",
"ICON_8",
]);

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 on lines +43 to +76

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

superRefine에서 반복 타입 불일치 시 stale 데이터 검증 누락

repeatTypeWEEKLY가 아닐 때 repeatWeekdays가 비어있는지, MONTHLY가 아닐 때 repeatDayOfMonth가 null인지 검증하지 않습니다. 사용자가 WEEKLY → DAILY로 변경하면 repeatWeekdays에 이전 선택값이 잔존할 수 있으며, 폼 hook(use-repeat-field.ts)에서 명시적으로 초기화하지 않으면 stale 데이터가 백엔드로 전송됩니다.

🛡️ stale 데이터 방지를 위한 superRefine 보강
   .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: "반복할 날짜를 선택해주세요.",
       });
     }
+
+    if (value.repeatType !== "WEEKLY" && value.repeatWeekdays?.length) {
+      ctx.addIssue({
+        code: "custom",
+        path: ["repeatWeekdays"],
+        message: "반복 요일은 WEEKLY 타입에서만 설정할 수 있습니다.",
+      });
+    }
+
+    if (value.repeatType !== "MONTHLY" && value.repeatDayOfMonth != null) {
+      ctx.addIssue({
+        code: "custom",
+        path: ["repeatDayOfMonth"],
+        message: "반복 날짜는 MONTHLY 타입에서만 설정할 수 있습니다.",
+      });
+    }
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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: "반복할 날짜를 선택해주세요.",
});
}
});
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: "반복할 날짜를 선택해주세요.",
});
}
if (value.repeatType !== "WEEKLY" && value.repeatWeekdays?.length) {
ctx.addIssue({
code: "custom",
path: ["repeatWeekdays"],
message: "반복 요일은 WEEKLY 타입에서만 설정할 수 있습니다.",
});
}
if (value.repeatType !== "MONTHLY" && value.repeatDayOfMonth != null) {
ctx.addIssue({
code: "custom",
path: ["repeatDayOfMonth"],
message: "반복 날짜는 MONTHLY 타입에서만 설정할 수 있습니다.",
});
}
});
🤖 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/api/todo/todo-schema.ts` around lines 43 - 76,
createTodoRequestSchema의 superRefine에서 repeatType과 반복 필드의 불일치를 검증하도록 보강하세요.
WEEKLY가 아닐 때 repeatWeekdays가 비어 있지 않으면 해당 값을 무효화하거나 검증 오류를 추가하고, MONTHLY가 아닐 때
repeatDayOfMonth가 null이 아니면 동일하게 처리하여 stale 데이터가 백엔드로 전송되지 않도록 하세요.


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 on lines +12 to +20

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

outline-none으로 포커스 테두리 제거 — 접근성 위반

outline-none이 포커스 시 시각적 표시기를 제거하여 키보드 사용자가 현재 포커스 위치를 파악하기 어렵습니다. 대체 포커스 스타일이 제공되지 않고 있으므로, focus-visible 기반 스타일을 추가해 주세요.

As per coding guidelines, "outline: none으로 포커스 테두리 제거 금지" .

🛡️ 포커스 스타일 추가 제안
-      className="typo-body-r-12 text-timo-gray-800 w-full resize-none outline-none"
+      className="typo-body-r-12 text-timo-gray-800 w-full resize-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-timo-blue-500"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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"
/>
);
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 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-timo-blue-500"
/>
);
🤖 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-modal/CreateTodoMemoField.tsx
around lines 12 - 20, textarea in CreateTodoMemoField currently removes the
focus outline without a replacement. Replace the unconditional outline-none
styling with a focus-visible-based accessible focus indicator, using
focus-visible:ring or an equivalent visible border/outline style while
preserving the default non-focused appearance.

Source: Path instructions

};
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 on lines +22 to +39

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

outline-none으로 포커스 테두리 제거 — 접근성 위반

title 입력(line 28)과 subtask 입력(line 37) 모두 outline-none을 사용하여 키보드 포커스 표시기가 사라집니다. focus-visible 기반 대체 스타일을 추가해 주세요.

As per coding guidelines, "outline: none으로 포커스 테두리 제거 금지" .

🛡️ 포커스 스타일 추가 제안
       <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"
+          className="typo-headline-b-14 text-timo-black min-w-0 flex-1 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-timo-blue-500"
         />
       </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"
+          className="typo-body-r-12 text-timo-gray-800 min-w-0 flex-1 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-timo-blue-500"
         />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<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>
<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 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-timo-blue-500"
/>
</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 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-timo-blue-500"
/>
</div>
🤖 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-modal/CreateTodoTaskFields.tsx
around lines 22 - 39, title and subtask inputs in CreateTodoTaskFields use
outline-none without a visible keyboard focus indicator. Replace the
unconditional outline removal on both inputs with focus-visible styles that
provide a clear focus outline or equivalent ring while preserving the existing
appearance otherwise.

Source: Path instructions

</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,11 +6,11 @@ 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";
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,89 @@
"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 { Modal } from "@/components/modal/Modal";

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 [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 (
<Modal
isOpen={isOpen}
onClose={onClose}
onExited={onExited}
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={t("createModal.close")}
onClick={onClose}
className="shrink-0"
>
<DeleteIcon />
</button>
Comment on lines +55 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.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

닫기 버튼 aria-label이 태그 모달이 아닌 투두 모달의 키를 참조합니다.

t("createModal.close")를 사용하고 있는데, createTagModal 네임스페이스에 별도의 close 키가 없어 재사용한 것으로 보입니다. 스크린 리더 사용자에게는 맥락이 다소 혼동될 수 있습니다. createTagModal.close 키를 추가하거나, 공통 Common.close 키를 사용하는 것이 더 일관성 있습니다.

🤖 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/_containers/tag-modal/CreateTagModalContainer.tsx
around lines 55 - 62, Update the close button in CreateTagModalContainer to use
a tag-modal-specific or shared close-label translation instead of
t("createModal.close"). Prefer adding and using the createTagModal.close key, or
use the existing Common.close key consistently with the translation structure.

</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}
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>
</Modal>
);
};
Loading