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(),