Skip to content

Commit f480b99

Browse files
committed
feat: 유저프로필 추가
1 parent 472a73b commit f480b99

File tree

5 files changed

+222
-25
lines changed

5 files changed

+222
-25
lines changed

src/apis/updateUserApi.ts

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import supabase from '@/supabase';
22

3-
const updateUserPassword = async (password: string) => {
3+
export const updateUserPassword = async (password: string) => {
44
const { data, error } = await supabase.auth.updateUser({ password });
55

66
if (error) {
@@ -10,4 +10,54 @@ const updateUserPassword = async (password: string) => {
1010
return data;
1111
};
1212

13-
export default updateUserPassword;
13+
14+
interface UpdateUserProfileParams {
15+
id: string;
16+
user_nickname: string | null;
17+
file: File | null;
18+
}
19+
20+
export const updateUserProfile = async ({ id, user_nickname, file }: UpdateUserProfileParams) => {
21+
let imageUrl: string | null = null;
22+
23+
const bucketName = 'profile';
24+
25+
if (file) {
26+
const filePath = `profile_images/${id}/${file.name}`;
27+
console.log(`Uploading file to path: ${filePath}`);
28+
29+
const { error: uploadError } = await supabase
30+
.storage
31+
.from(bucketName)
32+
.upload(filePath, file, { upsert: true });
33+
34+
if (uploadError) {
35+
console.error(`File upload error: ${uploadError.message}`);
36+
throw new Error(`File upload error: ${uploadError.message}`);
37+
}
38+
39+
const { data } = supabase
40+
.storage
41+
.from(bucketName)
42+
.getPublicUrl(filePath);
43+
44+
if (!data || !data.publicUrl) {
45+
console.error(`Unable to get public URL for file: ${filePath}`);
46+
throw new Error(`Unable to get public URL for file: ${filePath}`);
47+
}
48+
49+
imageUrl = data.publicUrl;
50+
}
51+
52+
const { data: updatedData, error: updateError } = await supabase
53+
.from('profiles')
54+
.update({ user_nickname, image_url: imageUrl })
55+
.eq('id', id);
56+
57+
if (updateError) {
58+
console.error(`Profile update error: ${updateError.message}`);
59+
throw new Error(`Profile update error: ${updateError.message}`);
60+
}
61+
62+
return updatedData;
63+
};

src/assets/svgs/ProfileIcon.tsx

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,27 @@
1-
const ProfileIcon = ({ imageUrl }: { imageUrl: string | null }) => {
1+
const ProfileIcon = ({
2+
imageUrl,
3+
width = "49", // 기본 가로 크기
4+
height = "49", // 기본 세로 크기
5+
fill = "white" // 기본 색상
6+
}: {
7+
imageUrl: string | null,
8+
width?: string,
9+
height?: string,
10+
fill?: string
11+
}) => {
212
return imageUrl === null ? (
3-
<svg width="49" height="49" viewBox="0 0 49 49" fill="none" xmlns="http://www.w3.org/2000/svg">
13+
<svg width={width} height={height} viewBox="0 0 49 49" fill="none" xmlns="http://www.w3.org/2000/svg">
414
<path
515
fillRule="evenodd"
616
clipRule="evenodd"
717
d="M40.9349 41.947C45.5732 37.5762 48.4688 31.3762 48.4688 24.5C48.4688 11.2624 37.7376 0.53125 24.5 0.53125C11.2624 0.53125 0.53125 11.2624 0.53125 24.5C0.53125 31.3762 3.42681 37.5762 8.06512 41.947C12.3561 45.9906 18.1387 48.4688 24.5 48.4688C30.8613 48.4688 36.6439 45.9906 40.9349 41.947ZM10.1068 38.7886C13.4857 34.5738 18.6777 31.875 24.5 31.875C30.3223 31.875 35.5143 34.5738 38.8932 38.7886C35.219 42.4896 30.1271 44.7812 24.5 44.7812C18.8729 44.7812 13.781 42.4896 10.1068 38.7886ZM33.7188 17.125C33.7188 22.2164 29.5914 26.3438 24.5 26.3438C19.4086 26.3438 15.2812 22.2164 15.2812 17.125C15.2812 12.0336 19.4086 7.90625 24.5 7.90625C29.5914 7.90625 33.7188 12.0336 33.7188 17.125Z"
8-
fill="white"
18+
fill={fill} // 동적으로 색상 변경
919
/>
1020
</svg>
1121
) : (
12-
<div className="avatar">
13-
<div className="w-24 rounded">
14-
<img src={imageUrl} />
22+
<div className="avatar overflow-hidden rounded-full">
23+
<div className="w-full rounded">
24+
<img src={imageUrl} alt="Profile" />
1525
</div>
1626
</div>
1727
);

src/pages/ProfilePage.tsx

Lines changed: 146 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,151 @@
1+
import { useState, FC, ChangeEvent } from 'react';
12
import { useGetProfile } from '@/react-queries/userGetProfile';
3+
import HistoryBackButton from '@/components/common/HistoryBackButton';
4+
import ProfileIcon from '@/assets/svgs/ProfileIcon';
5+
// import { useMutation } from '@tanstack/react-query';
6+
import { updateUserProfile } from '@/apis/updateUserApi';
27

3-
const ProfilePage = () => {
4-
const { data: user, error, isLoading, isError } = useGetProfile();
5-
console.log(user);
6-
7-
if (isError) {
8-
// TODO: 추후 에러 처리
9-
console.error(error);
10-
}
11-
12-
return (
13-
<div>
14-
{isLoading && <span className="loading" />}
15-
<h1>프로필</h1>
16-
<p>여기는 프로필 페이지입니다.</p>
17-
</div>
18-
);
8+
interface Props {
9+
userProfile?: {
10+
imageUrl: string | null;
11+
};
12+
}
13+
14+
const ProfilePage: FC<Props> = ({ userProfile }) => {
15+
const { data: user, error, isLoading, isError } = useGetProfile();
16+
const [editMode, setEditMode] = useState(false);
17+
const [imageUrl, setImageUrl] = useState(userProfile ? userProfile?.imageUrl : null);
18+
const [nickname, setNickname] = useState(user ? user?.user_nickname : '');
19+
const [selectedFile, setSelectedFile] = useState<File | null>(null);
20+
21+
const handleImageChange = (event: ChangeEvent<HTMLInputElement>) => {
22+
const file = event.target.files ? event.target.files[0] : null;
23+
if (file) {
24+
const reader = new FileReader();
25+
reader.onloadend = () => {
26+
setImageUrl(reader.result as string);
27+
};
28+
reader.readAsDataURL(file);
29+
setSelectedFile(file);
30+
}
31+
};
32+
33+
34+
const toggleEditMode = () => {
35+
setEditMode(!editMode);
36+
};
37+
38+
const resetData = () => {
39+
if (user) {
40+
setNickname(user.user_nickname);
41+
setImageUrl(userProfile ? userProfile.imageUrl : null);
42+
setSelectedFile(null);
43+
}
44+
setEditMode(false);
45+
};
46+
47+
const updateProfile = async () => {
48+
if (!user) return;
49+
50+
const param = {
51+
id: user.id,
52+
user_nickname: nickname,
53+
file: selectedFile,
54+
};
55+
56+
try {
57+
const updatedData = await updateUserProfile(param);
58+
console.log('프로필 업데이트 성공:', updatedData);
59+
setEditMode(false);
60+
} catch (error) {
61+
console.error('프로필 업데이트 실패:', error);
62+
}
63+
};
64+
65+
if (isError) {
66+
// TODO: 추후 에러 처리
67+
console.error(error);
68+
}
69+
70+
return (
71+
<div className="flex min-h-dvh w-screen flex-col overflow-x-hidden px-6">
72+
{isLoading && <span className="loading" />}
73+
<nav className="navHeight w-full py-5 flex justify-between items-center px-5">
74+
<HistoryBackButton />
75+
<h1 className="text-xl font-semibold" hidden>프로필</h1>
76+
{/* 편집버튼 */}
77+
<button type="button" onClick={toggleEditMode} hidden={editMode}>
78+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
79+
<path d="M16.8617 4.48667L18.5492 2.79917C19.2814 2.06694 20.4686 2.06694 21.2008 2.79917C21.9331 3.53141 21.9331 4.71859 21.2008 5.45083L6.83218 19.8195C6.30351 20.3481 5.65144 20.7368 4.93489 20.9502L2.25 21.75L3.04978 19.0651C3.26323 18.3486 3.65185 17.6965 4.18052 17.1678L16.8617 4.48667ZM16.8617 4.48667L19.5 7.12499" stroke="#0F172A" strokeWidth="1.5" strokeLinejoin="round"/>
80+
</svg>
81+
</button>
82+
</nav>
83+
<div className="container mx-auto flex max-w-sm flex-1 flex-col gap-4 pb-[50px] pt-4">
84+
<div className="w-80 h-80 flex justify-center items-center relative mx-auto my-5">
85+
{/* <button type="button" hidden={!editMode} className="absolute right-2.5 top-0" onClick={() => handleImageButton()}>
86+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
87+
<path d="M16.8617 4.48667L18.5492 2.79917C19.2814 2.06694 20.4686 2.06694 21.2008 2.79917C21.9331 3.53141 21.9331 4.71859 21.2008 5.45083L6.83218 19.8195C6.30351 20.3481 5.65144 20.7368 4.93489 20.9502L2.25 21.75L3.04978 19.0651C3.26323 18.3486 3.65185 17.6965 4.18052 17.1678L16.8617 4.48667ZM16.8617 4.48667L19.5 7.12499" stroke="#0F172A" strokeWidth="1.5" strokeLinejoin="round"/>
88+
</svg>
89+
</button> */}
90+
<ProfileIcon imageUrl={imageUrl} width="100%" height="100%" fill="#CCCFC4" />
91+
{editMode &&
92+
<label htmlFor="img" className="absolute inset-0 flex items-center justify-center bg-black bg-opacity-50 text-white flex-col gap-2.5 rounded-full">
93+
<span>이미지를 드래그해서 넣어주세요!</span>
94+
<input type="file" id="img" className="file-input file-input-bordered file-input-success file-input-sm w-4/5 max-w-xs" accept="image/*" onChange={(e) => handleImageChange(e)} />
95+
</label>
96+
}
97+
</div>
98+
<div className="info-form flex flex-col gap-1 mt-8">
99+
<div className="flex justify-between items-center">
100+
<span>이름</span>
101+
<input
102+
type="text"
103+
value={user?.user_name}
104+
readOnly
105+
className="p-2 bg-transparent"
106+
/>
107+
</div>
108+
<div className="flex justify-between items-center">
109+
<span>닉네임</span>
110+
<input
111+
type="text"
112+
value={nickname as string}
113+
onChange={(e) => setNickname(e.target.value)}
114+
readOnly={!editMode}
115+
className={`p-2 ${!editMode ? 'bg-transparent' : ''}`}
116+
/>
117+
</div>
118+
{/* <div className="flex justify-between items-center">
119+
<span>이메일</span>
120+
<input
121+
type="text"
122+
value={user?.user_name}
123+
placeholder="이메일"
124+
readOnly
125+
className="p-2 bg-transparent"
126+
/>
127+
</div> */}
128+
<div className="flex justify-between items-center">
129+
<span>전화번호</span>
130+
<input
131+
type="text"
132+
value={user?.phone}
133+
readOnly
134+
className="p-2 bg-transparent"
135+
/>
136+
</div>
137+
</div>
138+
139+
{ editMode &&
140+
<div className="flex gap-2 mt-20">
141+
<button className="btn btn-outline btn-primary flex-1" onClick={() => resetData()}>취소</button>
142+
<button type="submit" className="btn btn-outline btn-primary flex-1" onClick={() => updateProfile()}>수정완료</button>
143+
</div>
144+
}
145+
</div>
146+
147+
</div>
148+
);
19149
};
20150

21151
export default ProfilePage;

src/react-queries/useUpdateUserPassword.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { isLogIn } from '@/apis/authApis';
2-
import updateUserPassword from '@/apis/updateUserApi';
2+
import { updateUserPassword } from '@/apis/updateUserApi';
33
import { useChangePasswordState } from '@/stores/changePasswordStore';
44
import supabase from '@/supabase';
55
import { LooseValidation, ValidateProcessor } from '@/utils/authUtils';

src/styles/index.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@
1111
}
1212
}
1313

14+
button {
15+
cursor: pointer;
16+
}
17+
18+
.navHeight {
19+
height: 64px;
20+
}
1421
.userInvite {
1522
display: flex;
1623
justify-content: center;

0 commit comments

Comments
 (0)