Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
9 changes: 9 additions & 0 deletions src/assets/logo/social-logo/plain/slack.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
75 changes: 75 additions & 0 deletions src/components/setting/NotificationSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { useState } from "react";

import Badge from "@/components/common/badge/Badge";
import Button from "@/components/common/button/Button";
import Card from "@/components/common/card/Card";
import Toggle from "@/components/common/toggle/Toggle";

import MailIcon from "@/assets/icon/common/mail.svg?react";
import BellIcon from "@/assets/icon/sidebar/notification.svg?react";
import SlackIcon from "@/assets/logo/social-logo/plain/slack.svg?react";

type TNotificationSectionProps = {
email: string;
};

export default function NotificationSection({
email,
}: TNotificationSectionProps) {
const [browserPush, setBrowserPush] = useState(false);
const [emailNotif, setEmailNotif] = useState(false);

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>
</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>
</div>
<div className="flex items-center gap-4">
{browserPush && <Badge variant="infoBlue">권한 허용됨</Badge>}
<Toggle
checked={browserPush}
onToggle={() => setBrowserPush((p) => !p)}
ariaLabel="브라우저 푸시 알림 켜기/끄기"
/>
</div>
</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>
</div>
<Toggle
checked={emailNotif}
onToggle={() => setEmailNotif((p) => !p)}
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>
</div>
<Button variant="outline" size="small" type="button">
연동
</Button>
Comment thread
jjjsun marked this conversation as resolved.
Resolved
</div>
</div>
</Card>
);
}
6 changes: 0 additions & 6 deletions src/constants/sidebarNav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import type { INavItem } from "@/types/navigation/navItem";

import AdsIcon from "@/assets/icon/sidebar/ads.svg?react";
import DashboardIcon from "@/assets/icon/sidebar/dashboard.svg?react";
import NotificationIcon from "@/assets/icon/sidebar/notification.svg?react";
import SettingsIcon from "@/assets/icon/sidebar/setting.svg?react";
import WorkspaceIcon from "@/assets/icon/sidebar/workspace.svg?react";

Expand Down Expand Up @@ -80,11 +79,6 @@ export const mainNav: INavItem[] = [
];

export const footerNav: INavItem[] = [
{
id: "notifications",
label: "알림",
icon: NotificationIcon,
},
{
id: "settings",
label: "설정",
Expand Down
114 changes: 82 additions & 32 deletions src/pages/setting/Setting.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
import { type ChangeEvent, useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
import { twMerge } from "tailwind-merge";

import type { IApiErrorResponse } from "@/types/common/common";

import { useImageUploader } from "@/hooks/common/useImageUploader";

import Button from "@/components/common/button/Button";
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";

type TSettingTab = "profile" | "security" | "notifications";

const TABS: { id: TSettingTab; label: string }[] = [
{ id: "profile", label: "프로필" },
{ id: "security", label: "보안" },
{ id: "notifications", label: "알림" },
];

interface IDraftOrganization {
name: string;
position: string;
Expand All @@ -30,6 +40,7 @@ interface ISavedProfile {
}

export default function Setting() {
const [activeTab, setActiveTab] = useState<TSettingTab>("profile");
const [savedProfile, setSavedProfile] = useState<ISavedProfile>({
name: "",
profileImageUrl: null,
Expand Down Expand Up @@ -63,20 +74,23 @@ export default function Setting() {
const hasPasswordChanges =
!!currentPassword || !!newPassword || !!confirmNewPassword;

const hasChanges = useMemo(() => {
const hasProfileChanges = useMemo(() => {
return (
savedProfile.name !== draftProfile.name ||
savedProfile.profileImageUrl !== preview ||
!!file ||
hasPasswordChanges
!!file
);
}, [savedProfile, draftProfile, preview, file, hasPasswordChanges]);
}, [savedProfile, draftProfile, preview, file]);

const hasChanges =
activeTab === "profile" ? hasProfileChanges : hasPasswordChanges;
Comment thread
jjjsun marked this conversation as resolved.
Resolved

const [passwordErrors, setPasswordErrors] = useState({
currentPassword: "",
newPassword: "",
confirmNewPassword: "",
});

const validatePassword = () => {
const errors = {
currentPassword: "",
Expand Down Expand Up @@ -172,43 +186,79 @@ export default function Setting() {
};
fetchMyInfo();
}, [setPreview]);

return (
<section className="w-full flex flex-col gap-8">
<nav aria-label="설정 탭" className="flex border-b border-surface-300">
{TABS.map((tab) => (
<button
key={tab.id}
type="button"
role="tab"
aria-selected={activeTab === tab.id}
onClick={() => setActiveTab(tab.id)}
className={twMerge(
"px-5 py-3 font-body1 transition-colors duration-200 border-b-2 -mb-px",
activeTab === tab.id
? "border-primary-400 text-primary-400"
: "border-transparent text-text-muted hover:text-text-title",
)}
>
{tab.label}
</button>
))}
</nav>

<div className="flex flex-col gap-6">
{isLoading ? (
{activeTab === "profile" && (
<>
<ProfileSectionSkeleton />
<PasswordSectionSkeleton />
{isLoading ? (
<ProfileSectionSkeleton />
) : (
<ProfileSection
name={draftProfile.name}
setName={(v) =>
setDraftProfile((prev) => ({ ...prev, name: v }))
}
organizations={draftProfile.organizations}
email={draftProfile.email}
phoneNumber={draftProfile.phoneNumber}
fileRef={fileRef}
preview={preview}
onPickFile={handlePickFile}
openFilePicker={openFilePicker}
resetImage={() => {
resetImage();
setIsImageDeleted(true);
}}
/>
)}
</>
) : (
)}

{activeTab === "security" && (
<>
<ProfileSection
name={draftProfile.name}
setName={(v) => setDraftProfile((prev) => ({ ...prev, name: v }))}
organizations={draftProfile.organizations}
email={draftProfile.email}
phoneNumber={draftProfile.phoneNumber}
fileRef={fileRef}
preview={preview}
onPickFile={handlePickFile}
openFilePicker={openFilePicker}
resetImage={() => {
resetImage();
setIsImageDeleted(true);
}}
/>
<PasswordSection
currentPassword={currentPassword}
setCurrentPassword={setCurrentPassword}
newPassword={newPassword}
setNewPassword={setNewPassword}
confirmNewPassword={confirmNewPassword}
setConfirmNewPassword={setConfirmNewPassword}
errors={passwordErrors}
/>
{isLoading ? (
<PasswordSectionSkeleton />
) : (
<PasswordSection
currentPassword={currentPassword}
setCurrentPassword={setCurrentPassword}
newPassword={newPassword}
setNewPassword={setNewPassword}
confirmNewPassword={confirmNewPassword}
setConfirmNewPassword={setConfirmNewPassword}
errors={passwordErrors}
/>
)}
</>
)}

{activeTab === "notifications" && (
<NotificationSection email={draftProfile.email} />
)}
Comment thread
jjjsun marked this conversation as resolved.
Resolved
</div>

<div className="flex justify-end">
<Button
variant="primary"
Expand Down
Loading