diff --git a/src/components/setting/NotificationSection.tsx b/src/components/setting/NotificationSection.tsx index e8af7c80..3424c538 100644 --- a/src/components/setting/NotificationSection.tsx +++ b/src/components/setting/NotificationSection.tsx @@ -3,21 +3,26 @@ import Button from "@/components/common/button/Button"; import Card from "@/components/common/card/Card"; import Toggle from "@/components/common/toggle/Toggle"; +import DownloadIcon from "@/assets/icon/common/download.svg?react"; import MailIcon from "@/assets/icon/common/mail.svg?react"; +import WarnCircleIcon from "@/assets/icon/common/warn-circle.svg?react"; import BellIcon from "@/assets/icon/sidebar/notification.svg?react"; import SlackIcon from "@/assets/logo/social-logo/plain/slack.svg?react"; -export interface INotificationSettings { - browserPush: boolean; - emailNotif: boolean; -} - type TNotificationSectionProps = { email: string; + //channel browserPush: boolean; emailNotif: boolean; onBrowserPushChange: (value: boolean) => void; onEmailNotifChange: (value: boolean) => void; + //workspace + workspaceName: string | null; // null이면 미선택/없음 + clickAlarm: boolean; + weeklyReport: boolean; + onClickAlarmChange: (value: boolean) => void; + onWeeklyReportChange: (value: boolean) => void; + workspaceNotifiDisabled: boolean; //selectedOrgId === null 등 }; export default function NotificationSection({ @@ -26,58 +31,145 @@ export default function NotificationSection({ emailNotif, onBrowserPushChange, onEmailNotifChange, + workspaceName, + clickAlarm, + weeklyReport, + onClickAlarmChange, + onWeeklyReportChange, + workspaceNotifiDisabled, }: TNotificationSectionProps) { + const rowIconClass = + "h-5 w-5 shrink-0 text-text-muted [&_path]:[stroke-width:2]"; + return (
-

알림 채널 설정

+

알림 설정

-
-
-
- -

브라우저 푸시 알림

+
+

+ 알림 채널 +

+ +
+
+
+ +

브라우저 푸시 알림

+
+
+ {browserPush && 켜짐} + onBrowserPushChange(!browserPush)} + ariaLabel="브라우저 푸시 알림 켜기/끄기" + /> +
+
+ +
+
+ +
+

이메일 알림 설정

+

{email}

+
+
+
+ {emailNotif && 켜짐} + onEmailNotifChange(!emailNotif)} + ariaLabel="이메일 알림 켜기/끄기" + /> +
-
- {browserPush && 켜짐} - onBrowserPushChange(!browserPush)} - ariaLabel="브라우저 푸시 알림 켜기/끄기" - /> + +
+
+ +
+

슬랙 연동하기

+

#채널명

+
+
+
+
+ +
+ +
+
+

+ 워크스페이스 알림 +

+

+ {workspaceNotifiDisabled + ? "워크스페이스를 선택해주세요" + : workspaceName} +

+
-
-
- -
-

이메일 알림 설정

-

{email}

+
+
+
+ +
+

클릭수 알림

+

+ 이상 징후가 있을 때 알림 +

+
+
+
+ {clickAlarm && !workspaceNotifiDisabled && ( + 켜짐 + )} + onClickAlarmChange(!clickAlarm)} + />
- onEmailNotifChange(!emailNotif)} - ariaLabel="이메일 알림 켜기/끄기" - /> -
-
-
- -
-

슬랙 연동하기

-

#채널명

+
+
+ +
+

주간 리포트

+

+ 매주 월요일 오전 8시 리포트 이메일 발송 +

+
+
+
+ {weeklyReport && !workspaceNotifiDisabled && ( + 켜짐 + )} + onWeeklyReportChange(!weeklyReport)} + />
-
-
+
); } diff --git a/src/pages/setting/Setting.tsx b/src/pages/setting/Setting.tsx index 5711f076..87c6c0fb 100644 --- a/src/pages/setting/Setting.tsx +++ b/src/pages/setting/Setting.tsx @@ -2,25 +2,36 @@ import { type ChangeEvent, useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; import type { IApiErrorResponse } from "@/types/common/common"; +import type { + IChannelNotificationSettings, + IWorkspaceNotificationSettings, +} from "@/types/setting/notification"; import { useImageUploader } from "@/hooks/common/useImageUploader"; +import { useCoreQuery } from "@/hooks/customQuery"; import Button from "@/components/common/button/Button"; -import NotificationSection, { - type INotificationSettings, -} from "@/components/setting/NotificationSection"; +import NotificationSection from "@/components/setting/NotificationSection"; import PasswordSection from "@/components/setting/PasswordSection"; import PasswordSectionSkeleton from "@/components/setting/PasswordSectionSkeleton"; import ProfileSection from "@/components/setting/ProfileSection"; import ProfileSectionSkeleton from "@/components/setting/ProfileSectionSkeleton"; import { getMyInfo, updateMyInfo } from "@/api/auth/auth"; +import { getMyWorkspaces } from "@/api/workspace/org"; +import { QUERY_KEYS } from "@/lib/queryKeys"; +import useWorkspaceStore from "@/store/useWorkspaceStore"; -const DEFAULT_NOTIFICATION_SETTINGS: INotificationSettings = { +const DEFAULT_CHANNEL: IChannelNotificationSettings = { browserPush: false, emailNotif: false, }; +const DEFAULT_WORKSPACE_NOTIF: IWorkspaceNotificationSettings = { + clickAlarm: false, + weeklyReport: false, +}; + interface IDraftProfile { name: string; email: string; @@ -45,10 +56,14 @@ export default function Setting() { const [currentPassword, setCurrentPassword] = useState(""); const [newPassword, setNewPassword] = useState(""); const [confirmNewPassword, setConfirmNewPassword] = useState(""); - const [savedNotification, setSavedNotification] = - useState(DEFAULT_NOTIFICATION_SETTINGS); - const [draftNotification, setDraftNotification] = - useState(DEFAULT_NOTIFICATION_SETTINGS); + const [savedChannel, setSavedChannel] = + useState(DEFAULT_CHANNEL); + const [draftChannel, setDraftChannel] = + useState(DEFAULT_CHANNEL); + const [savedWorkspaceNotif, setSavedWorkspaceNotif] = + useState(DEFAULT_WORKSPACE_NOTIF); + const [draftWorkspaceNotif, setDraftWorkspaceNotif] = + useState(DEFAULT_WORKSPACE_NOTIF); const [isImageDeleted, setIsImageDeleted] = useState(false); const [isLoading, setIsLoading] = useState(false); const { @@ -61,6 +76,19 @@ export default function Setting() { resetImage, } = useImageUploader(); + const selectedOrgId = useWorkspaceStore((s) => s.selectedOrgId); + const { data: workspaces, isLoading: isWorkspacesLoading } = useCoreQuery( + QUERY_KEYS.workspace.list(), + getMyWorkspaces, + ); + const currentWorkspaceName = useMemo(() => { + if (selectedOrgId === null) return null; + return workspaces?.find((w) => w.orgId === selectedOrgId)?.name ?? null; + }, [selectedOrgId, workspaces]); + + const workspaceNotifiDisabled = + selectedOrgId == null || (!isWorkspacesLoading && !currentWorkspaceName); + const handlePickFile = (e: ChangeEvent) => { setIsImageDeleted(false); onPickFile(e); @@ -77,15 +105,24 @@ export default function Setting() { ); }, [savedProfile, draftProfile, preview, file]); - const hasNotificationChanges = useMemo(() => { + const hasChannelChanges = useMemo(() => { + return ( + savedChannel.browserPush !== draftChannel.browserPush || + savedChannel.emailNotif !== draftChannel.emailNotif + ); + }, [savedChannel, draftChannel]); + + const hasWorkspaceNotifChanges = useMemo(() => { return ( - savedNotification.browserPush !== draftNotification.browserPush || - savedNotification.emailNotif !== draftNotification.emailNotif + savedWorkspaceNotif.clickAlarm !== draftWorkspaceNotif.clickAlarm || + savedWorkspaceNotif.weeklyReport !== draftWorkspaceNotif.weeklyReport ); - }, [savedNotification, draftNotification]); + }, [savedWorkspaceNotif, draftWorkspaceNotif]); const hasAccountChanges = hasProfileChanges || hasPasswordChanges; + const hasNotificationChanges = hasChannelChanges || hasWorkspaceNotifChanges; + const hasChanges = hasAccountChanges || hasNotificationChanges; const [passwordErrors, setPasswordErrors] = useState({ @@ -154,15 +191,23 @@ export default function Setting() { setIsImageDeleted(false); } - if (hasNotificationChanges) { - setSavedNotification(draftNotification); + if (hasChannelChanges) { + setSavedChannel(draftChannel); // TODO: 알림 설정 API 연동 } + if (hasWorkspaceNotifChanges && selectedOrgId != null) { + setSavedWorkspaceNotif(draftWorkspaceNotif); + } + + const savedWorkspaceNotifiThisTime = + hasWorkspaceNotifChanges && selectedOrgId != null; + const savedAnyNotification = + hasChannelChanges || savedWorkspaceNotifiThisTime; toast.success( - hasAccountChanges && hasNotificationChanges + hasAccountChanges && savedAnyNotification ? "설정이 저장되었습니다" - : hasNotificationChanges + : savedAnyNotification ? "알림 설정이 저장되었습니다" : "회원정보가 수정되었습니다", ); @@ -199,6 +244,11 @@ export default function Setting() { fetchMyInfo(); }, [setPreview]); + useEffect(() => { + setSavedWorkspaceNotif(DEFAULT_WORKSPACE_NOTIF); + setDraftWorkspaceNotif(DEFAULT_WORKSPACE_NOTIF); + }, [selectedOrgId]); + return (
@@ -240,14 +290,27 @@ export default function Setting() { ) : ( - setDraftNotification((prev) => ({ ...prev, browserPush: value })) + setDraftChannel((prev) => ({ ...prev, browserPush: value })) } onEmailNotifChange={(value) => - setDraftNotification((prev) => ({ ...prev, emailNotif: value })) + setDraftChannel((prev) => ({ ...prev, emailNotif: value })) + } + workspaceName={currentWorkspaceName} + clickAlarm={draftWorkspaceNotif.clickAlarm} + weeklyReport={draftWorkspaceNotif.weeklyReport} + onClickAlarmChange={(value) => + setDraftWorkspaceNotif((prev) => ({ ...prev, clickAlarm: value })) + } + onWeeklyReportChange={(value) => + setDraftWorkspaceNotif((prev) => ({ + ...prev, + weeklyReport: value, + })) } + workspaceNotifiDisabled={workspaceNotifiDisabled} /> )}
diff --git a/src/types/setting/notification.ts b/src/types/setting/notification.ts new file mode 100644 index 00000000..59ed5295 --- /dev/null +++ b/src/types/setting/notification.ts @@ -0,0 +1,9 @@ +export interface IChannelNotificationSettings { + browserPush: boolean; + emailNotif: boolean; +} + +export interface IWorkspaceNotificationSettings { + clickAlarm: boolean; + weeklyReport: boolean; +}