diff --git a/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/toast/TagLimitToastContainer.tsx b/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/toast/TagLimitToastContainer.tsx index e83a64d1..ef832d76 100644 --- a/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/toast/TagLimitToastContainer.tsx +++ b/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/toast/TagLimitToastContainer.tsx @@ -3,18 +3,33 @@ import { useTranslations } from "next-intl"; import { useState } from "react"; import { AnimatedToast } from "@/components/toast/AnimatedToast"; +import { MAX_CUSTOM_TAG_COUNT } from "@/schemas/tag/tag-schema"; -export const TagLimitToastContainer = () => { +interface TagLimitToastContainerProps { + count?: number; + onClose?: () => void; +} + +export const TagLimitToastContainer = ({ + count = MAX_CUSTOM_TAG_COUNT, + onClose, +}: TagLimitToastContainerProps) => { const [isOpen, setIsOpen] = useState(true); const t = useTranslations("Toast"); + const handleClose = () => { + setIsOpen(false); + onClose?.(); + }; + return ( setIsOpen(false)} + onClose={handleClose} message={

{t.rich("tagLimit", { + count, blue: (chunks) => ( {chunks} ), diff --git a/apps/timo-web/app/[locale]/(main)/settings/_containers/account/SettingsProfileContainer.tsx b/apps/timo-web/app/[locale]/(main)/settings/_containers/account/SettingsProfileContainer.tsx index 8e1f8d8c..607386ae 100644 --- a/apps/timo-web/app/[locale]/(main)/settings/_containers/account/SettingsProfileContainer.tsx +++ b/apps/timo-web/app/[locale]/(main)/settings/_containers/account/SettingsProfileContainer.tsx @@ -4,11 +4,13 @@ import { useTranslations } from "next-intl"; import { overlay } from "overlay-kit"; import { useState } from "react"; +import { TagLimitToastContainer } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_containers/toast/TagLimitToastContainer"; import { SettingsProfileView } from "@/app/[locale]/(main)/settings/_components/account/SettingsProfileView"; import { useSettingsProfile } from "@/app/[locale]/(main)/settings/_hooks/account/use-settings-profile"; import { useSettingsProfileLabels } from "@/app/[locale]/(main)/settings/_hooks/account/use-settings-profile-labels"; import { CreateTagModalContainer } from "@/components/tag/CreateTagModalContainer"; import { AnimatedToast } from "@/components/toast/AnimatedToast"; +import { MAX_CUSTOM_TAG_COUNT } from "@/schemas/tag/tag-schema"; export const SettingsProfileContainer = () => { const tToast = useTranslations("Toast"); @@ -20,6 +22,7 @@ export const SettingsProfileContainer = () => { profileState.calendarConnected, ); const [isTagErrorToastOpen, setIsTagErrorToastOpen] = useState(false); + const [isTagLimitToastOpen, setIsTagLimitToastOpen] = useState(false); const [isLanguageErrorToastOpen, setIsLanguageErrorToastOpen] = useState(false); @@ -37,6 +40,15 @@ export const SettingsProfileContainer = () => { }; const handleAddTag = () => { + const customTagCount = profileState.tags.filter( + (tag) => !tag.isDefault, + ).length; + + if (customTagCount >= MAX_CUSTOM_TAG_COUNT) { + setIsTagLimitToastOpen(true); + return; + } + const existingLabels = profileState.tags.map((tag) => tag.label); overlay.open(({ isOpen, close, unmount }) => ( @@ -78,6 +90,10 @@ export const SettingsProfileContainer = () => { onLogout={profileActions.onLogout} /> + {isTagLimitToastOpen && ( + setIsTagLimitToastOpen(false)} /> + )} + setIsTagErrorToastOpen(false)} diff --git a/apps/timo-web/app/[locale]/(main)/settings/_hooks/account/use-settings-profile.ts b/apps/timo-web/app/[locale]/(main)/settings/_hooks/account/use-settings-profile.ts index d39378c0..8ba2db38 100644 --- a/apps/timo-web/app/[locale]/(main)/settings/_hooks/account/use-settings-profile.ts +++ b/apps/timo-web/app/[locale]/(main)/settings/_hooks/account/use-settings-profile.ts @@ -16,7 +16,10 @@ import { useMyProfileQuery } from "@/queries/auth/use-my-profile-query"; import { useCreateTagMutation } from "@/queries/tag/use-create-tag-mutation"; import { useDeleteTagMutation } from "@/queries/tag/use-delete-tag-mutation"; import { useTagsQuery } from "@/queries/tag/use-tags-query"; -import { tagCreateDataSchema } from "@/schemas/tag/tag-schema"; +import { + MAX_CUSTOM_TAG_COUNT, + tagCreateDataSchema, +} from "@/schemas/tag/tag-schema"; import { getDefaultTagLabelKey } from "@/utils/todo/tag-label"; const LANGUAGE_REQUEST_MAP: Record< @@ -87,6 +90,13 @@ export const useSettingsProfile = () => { }; const handleCreateTag = (label: string, handlers: CreateTagHandlers) => { + const customTagCount = tagItems.filter((tag) => !tag.isDefault).length; + + if (customTagCount >= MAX_CUSTOM_TAG_COUNT) { + handlers.onError(); + return; + } + createTag( { name: label }, { diff --git a/apps/timo-web/components/todo-modal/create/CreateTodoModalContent.tsx b/apps/timo-web/components/todo-modal/create/CreateTodoModalContent.tsx index 3847b648..2af5b173 100644 --- a/apps/timo-web/components/todo-modal/create/CreateTodoModalContent.tsx +++ b/apps/timo-web/components/todo-modal/create/CreateTodoModalContent.tsx @@ -9,6 +9,7 @@ import { useController, useForm } from "react-hook-form"; import type { CreateTodoRequest } from "@/schemas/todo/todo-schema"; import type { PriorityLevel } from "@repo/timo-design-system/ui"; +import { TagLimitToastContainer } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_containers/toast/TagLimitToastContainer"; import { OverlayModal } from "@/components/modal/OverlayModal"; import { AnimatedToast } from "@/components/toast/AnimatedToast"; import { TodoIconField } from "@/components/todo-modal/common/TodoIconField"; @@ -214,19 +215,9 @@ export const CreateTodoModalContent = ({ - - {tToast.rich("tagLimit", { - blue: (chunks) => ( - {chunks} - ), - })} -

- } - /> + {tagField.isTagLimitToastOpen && ( + + )} - - {tToast.rich("tagLimit", { - blue: (chunks) => ( - {chunks} - ), - })} -

- } - /> + {detailTodoForm.isTagLimitToastOpen && ( + + )} { control: Control; name?: Path; @@ -61,6 +62,9 @@ export const useTagField = ({ const selectedTagOption = tagOptions.find( (option) => option.id === field.value, ); + const customTagCount = (tagsQuery.data?.tags ?? []).filter( + (tag) => !tag.isDefault, + ).length; const handleSelectTag = (label: string) => { const option = tagOptions.find((item) => item.label === label); @@ -74,7 +78,7 @@ export const useTagField = ({ }; const handleAddTagClick = () => { - if (tagOptions.length >= MAX_TAG_COUNT) { + if (customTagCount >= MAX_CUSTOM_TAG_COUNT) { setIsTagLimitToastOpen(true); return; } diff --git a/apps/timo-web/messages/en.json b/apps/timo-web/messages/en.json index eb0745ef..125ac9c0 100644 --- a/apps/timo-web/messages/en.json +++ b/apps/timo-web/messages/en.json @@ -144,7 +144,7 @@ } }, "Toast": { - "tagLimit": "You can create up to 8 tags, and to add more, please delete existing tags.", + "tagLimit": "You can create up to {count} tags, and to add more, please delete existing tags.", "todoLimitLine1": "You can add up to 20 to-dos per day.", "todoLimitLine2": "To add a new to-do, please delete an existing one.", "onboardingSubmitFailed": "Failed to save onboarding. Please try again.", diff --git a/apps/timo-web/messages/ko.json b/apps/timo-web/messages/ko.json index 0e8040b4..0e82016e 100644 --- a/apps/timo-web/messages/ko.json +++ b/apps/timo-web/messages/ko.json @@ -144,7 +144,7 @@ } }, "Toast": { - "tagLimit": "태그는 최대 8개까지 만들 수 있으며, 추가하려면 기존 태그를 삭제해 주세요.", + "tagLimit": "태그는 최대 {count}개까지 만들 수 있으며, 추가하려면 기존 태그를 삭제해 주세요.", "todoLimitLine1": "투두는 하루에 최대 20개까지 추가할 수 있어요.", "todoLimitLine2": "새로운 투두를 추가하려면 기존 투두를 삭제해주세요.", "onboardingSubmitFailed": "온보딩 저장에 실패했어요. 다시 시도해 주세요.", diff --git a/apps/timo-web/schemas/tag/tag-schema.ts b/apps/timo-web/schemas/tag/tag-schema.ts index 349f043a..60d0787b 100644 --- a/apps/timo-web/schemas/tag/tag-schema.ts +++ b/apps/timo-web/schemas/tag/tag-schema.ts @@ -1,5 +1,7 @@ import { z } from "zod"; +export const MAX_CUSTOM_TAG_COUNT = 4; + export const tagSchema = z.object({ tagId: z.number(), name: z.string(),