Skip to content

Commit 95e4ecf

Browse files
Merge pull request #145 from Codeit-FE18-Part3-Team4/feature/#101
[#101] 계정관리 모달창 구현
2 parents eb8b06b + 256183a commit 95e4ecf

9 files changed

Lines changed: 467 additions & 10 deletions

File tree

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
.body {
2+
display: flex;
3+
flex-direction: column;
4+
gap: 30px;
5+
color: var(--color-gray300);
6+
}
7+
8+
.profileImageSection {
9+
display: flex;
10+
align-items: center;
11+
}
12+
13+
.profileImage > div > div,
14+
.profileImage img {
15+
width: 120px;
16+
height: 120px;
17+
object-fit: cover;
18+
}
19+
20+
.emailSection {
21+
display: flex;
22+
flex-direction: column;
23+
gap: 12px;
24+
}
25+
26+
.nicknameSection {
27+
display: flex;
28+
flex-direction: column;
29+
gap: 12px;
30+
}
31+
32+
.passwordSection {
33+
display: flex;
34+
flex-direction: column;
35+
align-items: flex-start;
36+
gap: 12px;
37+
}
38+
39+
.profileImageButtons {
40+
display: flex;
41+
gap: 12px;
42+
margin-left: 20px;
43+
}
44+
45+
.invisibleFileInput {
46+
display: none;
47+
}
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import Button, { ButtonSize, ButtonVariant } from "@/components/button/button";
2+
import Dialog from "@/components/dialog";
3+
import Input, { InputSize, InputVariant } from "@/components/input/input";
4+
import Profile from "@/components/profile/profile";
5+
import { ProfileSize } from "@/components/profile/profile-size";
6+
import Sheet, { SheetActionType } from "@/components/sheet";
7+
import { useAuth } from "@/features/auth/components/auth-provider";
8+
import { changeUserdata } from "@/features/user/apis/change-userdata";
9+
import { getMe, GetMeResponse } from "@/features/user/apis/get-me";
10+
import { useDialog } from "@/hooks/use-dialog";
11+
import { useModal } from "@/hooks/use-modal";
12+
import { AxiosError } from "axios";
13+
import { useEffect, useRef, useState } from "react";
14+
import styles from "./account-setting-modal.module.css";
15+
import PasswordChangeModal from "./password-change-modal";
16+
17+
interface AccountSettingModalProps {
18+
modalKey: string;
19+
}
20+
21+
export default function AccountSettingModal({
22+
modalKey,
23+
}: AccountSettingModalProps) {
24+
const PASSWORD_CHANGE_MODAL_KEY = "PASSWORD_CHANGE_MODAL";
25+
const {
26+
isShowModal: isShowPasswordChangeModal,
27+
openModal: openPasswordChangeModal,
28+
} = useModal({ key: PASSWORD_CHANGE_MODAL_KEY });
29+
const [userData, setUserData] = useState<GetMeResponse | null>(null);
30+
const [nickname, setNickname] = useState("");
31+
const [profileImage, setProfileImage] = useState("");
32+
const { isLoadingToken } = useAuth();
33+
const fileInputRef = useRef<HTMLInputElement | null>(null);
34+
const DIALOG_KEY = "DIALOG_CHAGNE_USERDATA";
35+
const { isShowDialog, openDialog } = useDialog({
36+
key: DIALOG_KEY,
37+
});
38+
const [dialogMessage, setDialogMessage] = useState("");
39+
const [selectedFile, setSelectedFile] = useState<File | null>(null);
40+
41+
useEffect(() => {
42+
if (isLoadingToken) return;
43+
async function loadUserData() {
44+
try {
45+
setUserData(await getMe());
46+
} catch (error: unknown) {
47+
console.error("Load User Data Failed:", error);
48+
}
49+
}
50+
loadUserData();
51+
}, [isLoadingToken]);
52+
53+
useEffect(() => {
54+
if (userData) {
55+
setNickname(userData.nickname);
56+
setProfileImage(userData.profileImageUrl);
57+
}
58+
}, [userData]);
59+
60+
const onNicknameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
61+
setNickname(e.target.value);
62+
};
63+
64+
const onFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
65+
const file = e.target.files?.[0];
66+
if (!file) return;
67+
const previewUrl = URL.createObjectURL(file);
68+
setProfileImage(previewUrl);
69+
setSelectedFile(file);
70+
};
71+
72+
const handleDeleteProfileImage = () => {
73+
setProfileImage("");
74+
};
75+
76+
const handleSubmit = async () => {
77+
try {
78+
await changeUserdata(nickname, profileImage);
79+
setDialogMessage("프로필이 성공적으로 변경되었습니다.");
80+
} catch (err) {
81+
const error = err as AxiosError<{ message?: string }>;
82+
const message = error.response?.data?.message;
83+
setDialogMessage(message || "프로필 변경에 실패했습니다.");
84+
} finally {
85+
openDialog(true);
86+
}
87+
};
88+
89+
return (
90+
<Sheet
91+
sheetKey={modalKey}
92+
title="프로필 변경"
93+
actionType={SheetActionType.Modify}
94+
onAction={handleSubmit}
95+
>
96+
<div className={styles.body}>
97+
<section className={styles.profileImageSection}>
98+
<div className={styles.profileImage}>
99+
<Profile
100+
profileImageUrl={profileImage as string}
101+
name={userData?.nickname}
102+
size={ProfileSize.XLarge}
103+
/>
104+
</div>
105+
<div className={styles.profileImageButtons}>
106+
<Button
107+
variant={ButtonVariant.Secondary}
108+
size={ButtonSize.Small}
109+
onClick={(e) => {
110+
e.preventDefault();
111+
fileInputRef.current?.click();
112+
}}
113+
>
114+
사진 변경
115+
</Button>
116+
<Button
117+
variant={ButtonVariant.Delete}
118+
size={ButtonSize.Small}
119+
onClick={(e) => {
120+
e.preventDefault();
121+
handleDeleteProfileImage();
122+
}}
123+
>
124+
사진 삭제
125+
</Button>
126+
<input
127+
className={styles.invisibleFileInput}
128+
ref={fileInputRef}
129+
type="file"
130+
accept="image/*"
131+
onChange={onFileChange}
132+
/>
133+
</div>
134+
</section>
135+
<section className={styles.emailSection}>
136+
<p>이메일</p>
137+
<Input
138+
variant={InputVariant.Default}
139+
$size={InputSize.Auto}
140+
disabled={true}
141+
placeholder={userData?.email || ""}
142+
/>
143+
</section>
144+
<section className={styles.nicknameSection}>
145+
<p>닉네임</p>
146+
<Input
147+
variant={InputVariant.Default}
148+
$size={InputSize.Auto}
149+
value={nickname}
150+
onChange={onNicknameChange}
151+
placeholder={userData?.nickname || ""}
152+
/>
153+
</section>
154+
<section className={styles.passwordSection}>
155+
<p>비밀번호</p>
156+
<Button
157+
variant={ButtonVariant.Secondary}
158+
size={ButtonSize.Small}
159+
onClick={(e) => {
160+
e.preventDefault();
161+
openPasswordChangeModal(true);
162+
}}
163+
>
164+
비밀번호 변경
165+
</Button>
166+
</section>
167+
</div>
168+
{isShowPasswordChangeModal && (
169+
<PasswordChangeModal modalKey={PASSWORD_CHANGE_MODAL_KEY} />
170+
)}
171+
172+
{isShowDialog && (
173+
<Dialog dialogKey={DIALOG_KEY} message={dialogMessage} />
174+
)}
175+
</Sheet>
176+
);
177+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
.body {
2+
display: flex;
3+
flex-direction: column;
4+
gap: 30px;
5+
color: var(--color-gray300);
6+
}
7+
8+
.inputSection {
9+
display: flex;
10+
flex-direction: column;
11+
gap: 12px;
12+
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import Dialog from "@/components/dialog";
2+
import Input, { InputSize, InputVariant } from "@/components/input/input";
3+
import Sheet, { SheetActionType } from "@/components/sheet";
4+
import { changePassword } from "@/features/user/apis/change-password";
5+
import { useDialog } from "@/hooks/use-dialog";
6+
import { useModal } from "@/hooks/use-modal";
7+
import { validatePassword } from "@/utils/validator";
8+
import { AxiosError } from "axios";
9+
import { useEffect, useState } from "react";
10+
import styles from "./password-change-modal.module.css";
11+
12+
interface PasswordChangeModalProps {
13+
modalKey: string;
14+
}
15+
16+
export default function PasswordChangeModal({
17+
modalKey,
18+
}: PasswordChangeModalProps) {
19+
const { openModal } = useModal({ key: modalKey });
20+
const [password, setPassword] = useState("");
21+
const [newPassword, setNewPassword] = useState("");
22+
const [newPasswordCheck, setNewPasswordCheck] = useState("");
23+
const [newPasswordErrorMessage, setNewPasswordErrorMessage] = useState("");
24+
const [newPasswordCheckErrorMessage, setNewPasswordCheckErrorMessage] =
25+
useState("");
26+
const onClose = () => openModal(false);
27+
const [isSubmitButtonDisabled, setIsSubmitButtonDisabled] = useState(true);
28+
const [dialogMessage, setDialogMessage] = useState("");
29+
const DIALOG_KEY = "DIALOG_CHAGNE_PASSWORD";
30+
const { isShowDialog, openDialog } = useDialog({
31+
key: DIALOG_KEY,
32+
});
33+
34+
const onNewPasswordBlur = () => {
35+
if (!newPassword) {
36+
setNewPasswordErrorMessage("");
37+
return;
38+
}
39+
40+
if (!validatePassword(newPassword)) {
41+
setNewPasswordErrorMessage("8자 이상 입력해주세요");
42+
return;
43+
}
44+
45+
if (newPassword !== newPasswordCheck) {
46+
setNewPasswordCheckErrorMessage("비밀번호가 일치하지 않습니다");
47+
} else {
48+
setNewPasswordCheckErrorMessage("");
49+
}
50+
};
51+
52+
const onNewPasswordCheckBlur = () => {
53+
if (!newPasswordCheck) {
54+
setNewPasswordCheckErrorMessage("");
55+
return;
56+
}
57+
if (newPassword === newPasswordCheck) {
58+
setNewPasswordCheckErrorMessage("");
59+
} else {
60+
setNewPasswordCheckErrorMessage("비밀번호가 일치하지 않습니다");
61+
}
62+
};
63+
64+
const onNewPasswordFocus = () => {
65+
setNewPasswordErrorMessage("");
66+
};
67+
const onNewPasswordCheckFocus = () => {
68+
setNewPasswordCheckErrorMessage("");
69+
};
70+
71+
useEffect(() => {
72+
const isFormValid =
73+
!!password &&
74+
!!newPassword &&
75+
validatePassword(newPassword) &&
76+
!!newPasswordCheck &&
77+
validatePassword(newPasswordCheck) &&
78+
newPassword === newPasswordCheck;
79+
80+
setIsSubmitButtonDisabled(!isFormValid);
81+
}, [password, newPassword, newPasswordCheck]);
82+
83+
const handleSubmit = async () => {
84+
try {
85+
const response = await changePassword({ password, newPassword });
86+
setDialogMessage("비밀번호가 성공적으로 변경되었습니다.");
87+
} catch (err) {
88+
const error = err as AxiosError<{ message?: string }>;
89+
const message = error.response?.data?.message;
90+
setDialogMessage(message || "비밀번호 변경에 실패했습니다.");
91+
} finally {
92+
openDialog(true);
93+
}
94+
};
95+
96+
return (
97+
<Sheet
98+
sheetKey={modalKey}
99+
title="비밀번호 변경"
100+
actionType={SheetActionType.Modify}
101+
canSubmit={!isSubmitButtonDisabled}
102+
onAction={handleSubmit}
103+
>
104+
<div className={styles.body}>
105+
<section className={styles.inputSection}>
106+
<p>현재 비밀번호</p>
107+
<Input
108+
variant={InputVariant.Default}
109+
$size={InputSize.Auto}
110+
type="password"
111+
value={password}
112+
onChange={(e) => setPassword(e.target.value)}
113+
placeholder="현재 비밀번호"
114+
/>
115+
</section>
116+
<section className={styles.inputSection}>
117+
<p>새 비밀번호</p>
118+
<Input
119+
variant={InputVariant.Password}
120+
$size={InputSize.Auto}
121+
type="password"
122+
value={newPassword}
123+
onChange={(e) => setNewPassword(e.target.value)}
124+
placeholder="새 비밀번호"
125+
onBlur={onNewPasswordBlur}
126+
errorMessage={newPasswordErrorMessage}
127+
onFocus={onNewPasswordFocus}
128+
/>
129+
</section>
130+
<section className={styles.inputSection}>
131+
<p>새 비밀번호 확인</p>
132+
<Input
133+
variant={InputVariant.Password}
134+
$size={InputSize.Auto}
135+
type="password"
136+
value={newPasswordCheck}
137+
onChange={(e) => setNewPasswordCheck(e.target.value)}
138+
placeholder="새 비밀번호 확인"
139+
onBlur={onNewPasswordCheckBlur}
140+
errorMessage={newPasswordCheckErrorMessage}
141+
onFocus={onNewPasswordCheckFocus}
142+
/>
143+
</section>
144+
</div>
145+
{isShowDialog && (
146+
<Dialog dialogKey={DIALOG_KEY} message={dialogMessage} />
147+
)}
148+
</Sheet>
149+
);
150+
}

0 commit comments

Comments
 (0)