Skip to content
Merged
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
172 changes: 132 additions & 40 deletions src/components/setting/NotificationSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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 (
<Card className="p-8 tablet:p-10">
<header className="mb-7 flex items-center gap-4">
<BellIcon />
<h2 className="font-heading4 text-text-title">알림 채널 설정</h2>
<h2 className="font-heading4 text-text-title">알림 설정</h2>
</header>

<div className="flex flex-col divide-y divide-surface-300">
<div className="flex items-center justify-between py-5 first:pt-0">
<div className="flex items-center gap-4">
<BellIcon className="h-5 w-5 shrink-0 text-text-muted" />
<p className="font-body1 text-text-title">브라우저 푸시 알림</p>
<section aria-labelledby="notification-channel-heading">
<h3
id="notification-channel-heading"
className="mb-4 font-label text-text-muted"
>
알림 채널
</h3>

<div className="flex flex-col divide-y divide-surface-300">
<div className="flex items-center justify-between py-5 first:pt-0">
<div className="flex items-center gap-4">
<BellIcon className={rowIconClass} />
<p className="font-body1 text-text-title">브라우저 푸시 알림</p>
</div>
<div className="flex items-center gap-4">
{browserPush && <Badge variant="infoBlue">켜짐</Badge>}
<Toggle
checked={browserPush}
onToggle={() => onBrowserPushChange(!browserPush)}
ariaLabel="브라우저 푸시 알림 켜기/끄기"
/>
</div>
</div>

<div className="flex items-center justify-between py-5">
<div className="flex items-center gap-4">
<MailIcon className={rowIconClass} />
<div>
<p className="font-body1 text-text-title">이메일 알림 설정</p>
<p className="font-body2 text-text-muted">{email}</p>
</div>
</div>
<div className="flex items-center gap-4">
{emailNotif && <Badge variant="infoBlue">켜짐</Badge>}
<Toggle
checked={emailNotif}
onToggle={() => onEmailNotifChange(!emailNotif)}
ariaLabel="이메일 알림 켜기/끄기"
/>
</div>
</div>
<div className="flex items-center gap-4">
{browserPush && <Badge variant="infoBlue">켜짐</Badge>}
<Toggle
checked={browserPush}
onToggle={() => onBrowserPushChange(!browserPush)}
ariaLabel="브라우저 푸시 알림 켜기/끄기"
/>

<div className="flex items-center justify-between py-5">
<div className="flex items-center gap-4">
<SlackIcon className={rowIconClass} />
<div>
<p className="font-body1 text-text-title">슬랙 연동하기</p>
<p className="font-body2 text-text-muted">#채널명</p>
</div>
</div>
<Button variant="outline" size="small" type="button">
연동
</Button>
</div>
</div>
</section>

<div className="my-8 border-t border-surface-300" aria-hidden />

<section aria-labelledby="notification-workspace-heading">
<div className="mb-4 flex items-center justify-between gap-3">
<h3
id="notification-workspace-heading"
className="font-label text-text-muted"
>
워크스페이스 알림
</h3>
<p className="min-w-0 truncate font-caption text-text-muted">
{workspaceNotifiDisabled
? "워크스페이스를 선택해주세요"
: workspaceName}
</p>
</div>

<div className="flex items-center justify-between py-5">
<div className="flex items-center gap-4">
<MailIcon className="h-5 w-5 shrink-0 text-text-muted" />
<div>
<p className="font-body1 text-text-title">이메일 알림 설정</p>
<p className="font-body2 text-text-muted">{email}</p>
<div className="flex flex-col divide-y divide-surface-300">
<div className="flex items-center justify-between gap-4 py-5 first:pt-0">
<div className="flex min-w-0 items-center gap-4">
<WarnCircleIcon className={rowIconClass} />
<div className="min-w-0">
<p className="font-body1 text-text-title">클릭수 알림</p>
<p className="font-body2 text-text-muted">
이상 징후가 있을 때 알림
</p>
</div>
</div>
<div className="flex shrink-0 items-center gap-4">
{clickAlarm && !workspaceNotifiDisabled && (
<Badge variant="infoBlue">켜짐</Badge>
)}
<Toggle
ariaLabel="클릭수 알람 켜기/끄기"
checked={clickAlarm}
disabled={workspaceNotifiDisabled}
onToggle={() => onClickAlarmChange(!clickAlarm)}
/>
</div>
</div>
<Toggle
checked={emailNotif}
onToggle={() => onEmailNotifChange(!emailNotif)}
ariaLabel="이메일 알림 켜기/끄기"
/>
</div>

<div className="flex items-center justify-between py-5 last:pb-0">
<div className="flex items-center gap-4">
<SlackIcon className="h-5 w-5 shrink-0" />
<div>
<p className="font-body1 text-text-title">슬랙 연동하기</p>
<p className="font-body2 text-text-muted">#채널명</p>
<div className="flex items-center justify-between gap-4 py-5 last:pb-0">
<div className="flex min-w-0 items-center gap-4">
<DownloadIcon className={rowIconClass} />
<div className="min-w-0">
<p className="font-body1 text-text-title">주간 리포트</p>
<p className="font-body2 text-text-muted">
매주 월요일 오전 8시 리포트 이메일 발송
</p>
</div>
</div>
<div className="flex shrink-0 items-center gap-4">
{weeklyReport && !workspaceNotifiDisabled && (
<Badge variant="infoBlue">켜짐</Badge>
)}
<Toggle
ariaLabel="주간리포트 받기/안받기"
checked={weeklyReport}
disabled={workspaceNotifiDisabled}
onToggle={() => onWeeklyReportChange(!weeklyReport)}
/>
</div>
</div>
<Button variant="outline" size="small" type="button">
연동
</Button>
</div>
</div>
</section>
</Card>
);
}
103 changes: 83 additions & 20 deletions src/pages/setting/Setting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -45,10 +56,14 @@ export default function Setting() {
const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [confirmNewPassword, setConfirmNewPassword] = useState("");
const [savedNotification, setSavedNotification] =
useState<INotificationSettings>(DEFAULT_NOTIFICATION_SETTINGS);
const [draftNotification, setDraftNotification] =
useState<INotificationSettings>(DEFAULT_NOTIFICATION_SETTINGS);
const [savedChannel, setSavedChannel] =
useState<IChannelNotificationSettings>(DEFAULT_CHANNEL);
const [draftChannel, setDraftChannel] =
useState<IChannelNotificationSettings>(DEFAULT_CHANNEL);
const [savedWorkspaceNotif, setSavedWorkspaceNotif] =
useState<IWorkspaceNotificationSettings>(DEFAULT_WORKSPACE_NOTIF);
const [draftWorkspaceNotif, setDraftWorkspaceNotif] =
useState<IWorkspaceNotificationSettings>(DEFAULT_WORKSPACE_NOTIF);
const [isImageDeleted, setIsImageDeleted] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const {
Expand All @@ -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);

Comment thread
jjjsun marked this conversation as resolved.
const handlePickFile = (e: ChangeEvent<HTMLInputElement>) => {
setIsImageDeleted(false);
onPickFile(e);
Expand All @@ -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({
Expand Down Expand Up @@ -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
? "알림 설정이 저장되었습니다"
: "회원정보가 수정되었습니다",
);
Comment thread
jjjsun marked this conversation as resolved.
Expand Down Expand Up @@ -199,6 +244,11 @@ export default function Setting() {
fetchMyInfo();
}, [setPreview]);

useEffect(() => {
setSavedWorkspaceNotif(DEFAULT_WORKSPACE_NOTIF);
setDraftWorkspaceNotif(DEFAULT_WORKSPACE_NOTIF);
}, [selectedOrgId]);

return (
<section className="w-full flex flex-col gap-8">
<div className="flex flex-col gap-6">
Expand Down Expand Up @@ -240,14 +290,27 @@ export default function Setting() {
) : (
<NotificationSection
email={draftProfile.email}
browserPush={draftNotification.browserPush}
emailNotif={draftNotification.emailNotif}
browserPush={draftChannel.browserPush}
emailNotif={draftChannel.emailNotif}
onBrowserPushChange={(value) =>
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}
/>
)}
</div>
Expand Down
Loading
Loading