Skip to content

Commit eb470ba

Browse files
authored
Merge pull request #92 from rover1523/page-jw
[Feat]프로필,비밀번호 변경 api작업
2 parents 522849c + b3bae84 commit eb470ba

File tree

9 files changed

+271
-41
lines changed

9 files changed

+271
-41
lines changed

src/api/apiRoutes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export const apiRoutes = {
44
//로그인
55
Login: () => `/${TEAM_ID}/login`, //post
66
//비밀번호변경
7-
Password: () => `/${TEAM_ID}/password`, //put
7+
Password: () => `/${TEAM_ID}/auth/password`, //put
88
//카드
99
Cards: () => `/${TEAM_ID}/cards`, //post,get
1010
CardDetail: (cardId: number) => `/${TEAM_ID}/cards/${cardId}`, //get,put,delete

src/api/changepassword.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import axios from "./axiosInstance";
2+
import { apiRoutes } from "./apiRoutes";
3+
import { isAxiosError } from "axios";
4+
5+
export const changePassword = async ({
6+
password,
7+
newPassword,
8+
}: {
9+
password: string;
10+
newPassword: string;
11+
}) => {
12+
try {
13+
const response = await axios.put(apiRoutes.Password(), {
14+
password,
15+
newPassword,
16+
});
17+
return { success: true, data: response.data };
18+
} catch (error) {
19+
if (isAxiosError(error)) {
20+
return {
21+
success: false,
22+
status: error.response?.status,
23+
message: error.response?.data?.message || "알 수 없는 오류",
24+
};
25+
}
26+
return {
27+
success: false,
28+
message: "에러가 발생했습니다.",
29+
};
30+
}
31+
};

src/api/userprofile.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { UpdateUser, UserType, UserMeImage } from "@/types/users";
2+
import axios from "./axiosInstance";
3+
import { apiRoutes } from "./apiRoutes";
4+
5+
// 내 정보 조회 (GET)
6+
export const getUserMe = async (): Promise<UserType> => {
7+
const response = await axios.get(apiRoutes.userMe());
8+
return response.data;
9+
};
10+
// 내 정보 수정 (PUT)
11+
export const updateProfile = async (data: UpdateUser) => {
12+
const res = await axios.put(apiRoutes.userMe(), data, {
13+
headers: {
14+
"Content-Type": "application/json",
15+
},
16+
});
17+
return res.data;
18+
};
19+
20+
// ✅ 프로필 이미지 업로드 (POST)
21+
export const uploadProfileImage = async (
22+
formData: FormData
23+
): Promise<UserMeImage> => {
24+
const response = await axios.post(apiRoutes.userMeImage(), formData, {
25+
headers: {
26+
"Content-Type": "multipart/form-data",
27+
},
28+
});
29+
return response.data;
30+
};

src/components/card/ChangePassword.tsx

Lines changed: 72 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,60 @@
11
import { useState } from "react";
2+
import { changePassword } from "@/api/changepassword";
3+
import MypageModal from "../modal/MypageModal";
24
import Input from "../input/Input";
5+
import Image from "next/image";
36

47
export default function ChangePassword() {
58
const [password, setPassword] = useState("");
69
const [newPassword, setNewPassword] = useState("");
710
const [checkNewpassword, setCheckNewPassword] = useState("");
8-
const [isPasswordMismatch] = useState(false);
9-
11+
const [isSubmitting, setIsSubmitting] = useState(false);
12+
const [errorMessage, setErrorMessage] = useState("");
13+
const [successMessage, setSuccessMessage] = useState("");
1014
const [showCheckNewPassword, setShowCheckNewPassword] = useState(false);
15+
const [showSuccessModal, setShowSuccessModal] = useState(false);
16+
const [showErrorModal, setShowErrorModal] = useState(false);
1117

1218
const toggleCheckNewPasswordVisibility = () =>
1319
setShowCheckNewPassword(!showCheckNewPassword);
14-
20+
const isPasswordMismatch =
21+
newPassword && checkNewpassword && newPassword !== checkNewpassword;
1522
const isDisabled =
1623
!password ||
1724
!newPassword ||
1825
!checkNewpassword ||
1926
isPasswordMismatch ||
2027
password.length < 8;
2128

29+
const handleChangePassword = async () => {
30+
if (isDisabled) return;
31+
32+
setIsSubmitting(true);
33+
setErrorMessage("");
34+
setSuccessMessage("");
35+
36+
const result = await changePassword({ password, newPassword });
37+
38+
if (!result.success) {
39+
const msg =
40+
result.status === 400
41+
? result.message || "현재 비밀번호가 올바르지 않습니다."
42+
: "비밀번호 변경 중 오류가 발생했습니다.";
43+
44+
setErrorMessage(msg);
45+
setShowErrorModal(true);
46+
setIsSubmitting(false);
47+
return;
48+
}
49+
50+
setSuccessMessage("비밀번호가 성공적으로 변경되었습니다.");
51+
setShowSuccessModal(true);
52+
setPassword("");
53+
setNewPassword("");
54+
setCheckNewPassword("");
55+
setIsSubmitting(false);
56+
};
57+
2258
return (
2359
<div className="sm:w-[672px] sm:h-[466px] w-[284px] h-[454px] bg-white rounded-[16px] shadow-md p-[24px] flex flex-col">
2460
<h2 className="text-[18px] sm:text-[24px] font-bold mb-4">
@@ -30,12 +66,12 @@ export default function ChangePassword() {
3066
<Input
3167
type="password"
3268
name="password"
33-
label="비밀번호"
69+
label="현재 비밀번호"
3470
labelClassName="font-16r"
35-
placeholder="비밀번호 입력"
71+
placeholder="현재 비밀번호 입력"
72+
value={password}
3673
onChange={setPassword}
3774
pattern=".{8,}"
38-
invalidMessage="8자 이상 입력해주세요."
3975
className="max-w-[620px]"
4076
/>
4177
<Input
@@ -44,6 +80,7 @@ export default function ChangePassword() {
4480
label="새 비밀번호"
4581
labelClassName="font-16r"
4682
placeholder="새 비밀번호 입력"
83+
value={newPassword}
4784
onChange={setNewPassword}
4885
pattern=".{8,}"
4986
invalidMessage="8자 이상 입력해주세요."
@@ -60,26 +97,28 @@ export default function ChangePassword() {
6097
placeholder="새 비밀번호 입력"
6198
onChange={(e) => setCheckNewPassword(e.target.value)}
6299
className={`mt-3 sm:w-[624px] sm:h-[50px] w-[236px] h-[50px] px-[16px] pr-12 rounded-[8px] transition-colors focus:outline-none
63-
${
64-
checkNewpassword
65-
? checkNewpassword === newPassword
66-
? "border border-gray-300"
67-
: "border border-[var(--color-red)]"
68-
: "border border-gray-300 focus:border-purple-500"
69-
}`}
100+
${
101+
checkNewpassword
102+
? checkNewpassword === newPassword
103+
? "border border-gray-300"
104+
: "border border-[var(--color-red)]"
105+
: "border border-gray-300 focus:border-purple-500"
106+
}`}
70107
/>
71108
<button
72109
type="button"
73110
onClick={toggleCheckNewPasswordVisibility}
74111
className="absolute right-4 top-6 flex size-6 items-center justify-center"
75112
>
76-
<img
113+
<Image
77114
src={
78115
showCheckNewPassword
79116
? "/svgs/eye-on.svg"
80117
: "/svgs/eye-off.svg"
81118
}
82119
alt="비밀번호 표시 토글"
120+
width={20}
121+
height={20}
83122
className=" w-5 h-5"
84123
/>
85124
</button>
@@ -93,11 +132,29 @@ export default function ChangePassword() {
93132
</div>
94133
</div>
95134

135+
{errorMessage && (
136+
<p className="text-red-500 text-sm mt-4">{errorMessage}</p>
137+
)}
138+
{successMessage && (
139+
<p className="text-green-600 text-sm mt-4">{successMessage}</p>
140+
)}
141+
<MypageModal
142+
isOpen={showSuccessModal}
143+
onClose={() => setShowSuccessModal(false)}
144+
message="비밀번호 변경에 성공하였습니다."
145+
/>
146+
147+
<MypageModal
148+
isOpen={showErrorModal}
149+
onClose={() => setShowErrorModal(false)}
150+
message="비밀번호 변경에 실패하였습니다."
151+
/>
96152
<button
97153
className={`mt-2.5 cursor-pointer w-full sm:w-[624px] h-[54px]
98154
rounded-[8px] text-lg font-medium
99155
${isDisabled ? "bg-gray-300 cursor-not-allowed" : "bg-[#5A3FFF] text-white"}`}
100-
disabled={isDisabled}
156+
onClick={() => handleChangePassword()}
157+
disabled={isDisabled || isSubmitting}
101158
>
102159
변경
103160
</button>

src/components/card/Profile.tsx

Lines changed: 81 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,89 @@
1-
import { useState } from "react";
1+
import { useState, useEffect } from "react";
22
import Input from "../input/Input";
3+
import Image from "next/image";
4+
import {
5+
getUserMe,
6+
updateProfile,
7+
uploadProfileImage,
8+
} from "@/api/userprofile";
9+
import MypageModal from "../modal/MypageModal";
310

411
export default function ProfileCard() {
512
const [image, setImage] = useState<string | null>(null);
613
const [nickname, setNickname] = useState("");
714
const [email, setEmail] = useState("");
15+
const [preview, setPreview] = useState<string | null>(null);
16+
const [showSuccessModal, setShowSuccessModal] = useState(false);
17+
const [showErrorModal, setShowErrorModal] = useState(false);
818

9-
const handleImageUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
19+
const fetchUserData = async () => {
20+
try {
21+
const data = await getUserMe();
22+
setImage(data.profileImageUrl);
23+
setNickname(data.nickname);
24+
setEmail(data.email);
25+
} catch (err) {
26+
console.error("유저 정보 불러오기 실패:", err);
27+
}
28+
};
29+
30+
const handleImageUpload = async (
31+
event: React.ChangeEvent<HTMLInputElement>
32+
) => {
1033
if (event.target.files && event.target.files[0]) {
11-
const reader = new FileReader();
12-
reader.onload = (e) => {
13-
const result = e.target?.result;
14-
if (typeof result === "string") {
15-
setImage(result);
16-
}
17-
};
18-
reader.readAsDataURL(event.target.files[0]);
34+
const file = event.target.files[0];
35+
setPreview(URL.createObjectURL(file)); // 미리보기
36+
37+
try {
38+
const formData = new FormData();
39+
formData.append("image", file);
40+
41+
const response = await uploadProfileImage(formData);
42+
setImage(response.profileImageUrl); // 서버에서 받은 URL 저장
43+
} catch (error) {
44+
console.error("이미지 업로드 실패:", error);
45+
alert("이미지 업로드에 실패했습니다.");
46+
}
47+
}
48+
};
49+
const handleSave = async () => {
50+
if (!nickname || !image) return;
51+
52+
const userProfile = {
53+
nickname,
54+
profileImageUrl: image,
55+
};
1956

20-
console.log(nickname, email);
57+
try {
58+
await updateProfile(userProfile);
59+
setShowSuccessModal(true);
60+
} catch (error) {
61+
console.error("프로필 저장 실패:", error);
62+
setShowErrorModal(true);
2163
}
2264
};
2365

66+
useEffect(() => {
67+
fetchUserData();
68+
}, []);
69+
2470
return (
2571
<div className="sm:w-[672px] sm:h-[366px] w-[284px] h-[496px] bg-white rounded-[16px] shadow-md p-[24px] flex flex-col">
2672
{/* 프로필 제목 */}
2773
<h2 className="text-[18px] sm:text-[24px] font-bold mb-4">프로필</h2>
28-
29-
{/* 프로필 이미지 + 입력 폼 > 컴포넌트 받으면 바꾸기*/}
74+
{/* 프로필 이미지 및 입력 폼 영역 */}
3075
<div className="flex flex-col sm:flex-row items-center sm:items-start">
3176
{/* 프로필 이미지 업로드 영역 */}
3277
<div className="sm:mr:0 mr-29 w-[120px] flex-shrink-0 mb-4 sm:mb-0">
3378
<div className="sm:w-[182px] sm:h-[182px] w-[100px] h-[100px] rounded-md flex items-center justify-center cursor-pointer bg-[#F5F5F5] border-transparent">
3479
<label className="cursor-pointer w-full h-full flex items-center justify-center">
3580
{image ? (
36-
<img
37-
src={image}
81+
<Image
82+
src={preview || image || ""}
3883
alt="Profile"
84+
width={182}
85+
height={182}
86+
unoptimized
3987
className="w-full h-full object-cover rounded-md"
4088
/>
4189
) : (
@@ -57,23 +105,34 @@ export default function ProfileCard() {
57105
name="email"
58106
label="이메일"
59107
labelClassName="font-16r"
60-
placeholder="이메일을 입력하세요"
61-
onChange={setEmail}
62-
pattern="^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
63-
invalidMessage="유효한 이메일 주소를 입력하세요."
108+
value={email}
109+
readOnly
64110
/>
65111

66112
<Input
67113
type="text"
68114
name="nickname"
69115
label="닉네임"
70116
labelClassName="font-16r"
117+
value={nickname}
71118
placeholder="닉네임을 입력하세요"
72-
onChange={setNickname}
119+
onChange={(value: string) => setNickname(value)}
120+
/>
121+
<MypageModal
122+
isOpen={showSuccessModal}
123+
onClose={() => setShowSuccessModal(false)}
124+
message="프로필 변경이 완료되었습니다."
125+
/>
126+
<MypageModal
127+
isOpen={showErrorModal}
128+
onClose={() => setShowErrorModal(false)}
129+
message="프로필 변경에 실패하였습니다"
73130
/>
74131

75-
{/* 저장 버튼 */}
76-
<button className="cursor-pointer w-full sm:w-[400px] h-[54px] bg-[#5A3FFF] text-white rounded-[8px] text-lg font-medium mt-3">
132+
<button
133+
className="cursor-pointer w-full sm:w-[400px] h-[54px] bg-[#5A3FFF] text-white rounded-[8px] text-lg font-medium mt-3"
134+
onClick={handleSave}
135+
>
77136
저장
78137
</button>
79138
</div>

src/components/input/Input.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ interface GeneralInputProps {
99
placeholder?: string;
1010
className?: string;
1111
onChange?: (value: string) => void;
12+
value?: string;
13+
readOnly?: boolean; //입력방지 추가
1214
}
1315

1416
interface SignInputProps extends Omit<GeneralInputProps, "type"> {

0 commit comments

Comments
 (0)