Skip to content

Commit ac243f5

Browse files
authored
Merge pull request #170 from imaginer-dev/164---ui-프로필-수정-구현
164 UI 프로필 수정 구현
2 parents 472a73b + 203f116 commit ac243f5

File tree

5 files changed

+212
-14
lines changed

5 files changed

+212
-14
lines changed

src/apis/updateUserApi.ts

Lines changed: 45 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,47 @@ const updateUserPassword = async (password: string) => {
1010
return data;
1111
};
1212

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

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: 143 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,157 @@
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 = () => {
8+
interface Props {
9+
userProfile?: {
10+
imageUrl: string | null;
11+
};
12+
}
13+
14+
const ProfilePage: FC<Props> = ({ userProfile }) => {
415
const { data: user, error, isLoading, isError } = useGetProfile();
5-
console.log(user);
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+
const toggleEditMode = () => {
34+
setEditMode(!editMode);
35+
};
36+
37+
const resetData = () => {
38+
if (user) {
39+
setNickname(user.user_nickname);
40+
setImageUrl(userProfile ? userProfile.imageUrl : null);
41+
setSelectedFile(null);
42+
}
43+
setEditMode(false);
44+
};
45+
46+
const updateProfile = async () => {
47+
if (!user) return;
48+
49+
const param = {
50+
id: user.id,
51+
user_nickname: nickname,
52+
file: selectedFile,
53+
};
54+
55+
try {
56+
const updatedData = await updateUserProfile(param);
57+
console.log('프로필 업데이트 성공:', updatedData);
58+
setEditMode(false);
59+
} catch (error) {
60+
console.error('프로필 업데이트 실패:', error);
61+
}
62+
};
663

764
if (isError) {
865
// TODO: 추후 에러 처리
966
console.error(error);
1067
}
1168

1269
return (
13-
<div>
70+
<div className="flex min-h-dvh w-screen flex-col overflow-x-hidden px-6">
1471
{isLoading && <span className="loading" />}
15-
<h1>프로필</h1>
16-
<p>여기는 프로필 페이지입니다.</p>
72+
<nav className="navHeight flex w-full items-center justify-between px-5 py-5">
73+
<HistoryBackButton />
74+
<h1 className="text-xl font-semibold" hidden>
75+
프로필
76+
</h1>
77+
{/* 편집버튼 */}
78+
<button type="button" onClick={toggleEditMode} hidden={editMode}>
79+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
80+
<path
81+
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"
82+
stroke="#0F172A"
83+
strokeWidth="1.5"
84+
strokeLinejoin="round"
85+
/>
86+
</svg>
87+
</button>
88+
</nav>
89+
<div className="container mx-auto flex max-w-sm flex-1 flex-col gap-4 pb-[50px] pt-4">
90+
<div className="relative mx-auto my-5 flex h-80 w-80 items-center justify-center">
91+
{/* <button type="button" hidden={!editMode} className="absolute right-2.5 top-0" onClick={() => handleImageButton()}>
92+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
93+
<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"/>
94+
</svg>
95+
</button> */}
96+
<ProfileIcon imageUrl={imageUrl} width="100%" height="100%" fill="#CCCFC4" />
97+
{editMode && (
98+
<label
99+
htmlFor="img"
100+
className="absolute inset-0 flex flex-col items-center justify-center gap-2.5 rounded-full bg-black bg-opacity-50 text-white"
101+
>
102+
<span>이미지를 드래그해서 넣어주세요!</span>
103+
<input
104+
type="file"
105+
id="img"
106+
className="file-input file-input-bordered file-input-success file-input-sm w-4/5 max-w-xs"
107+
accept="image/*"
108+
onChange={(e) => handleImageChange(e)}
109+
/>
110+
</label>
111+
)}
112+
</div>
113+
<div className="info-form mt-8 flex flex-col gap-1">
114+
<div className="flex items-center justify-between">
115+
<span>이름</span>
116+
<input type="text" value={user?.user_name} readOnly className="bg-transparent p-2" />
117+
</div>
118+
<div className="flex items-center justify-between">
119+
<span>닉네임</span>
120+
<input
121+
type="text"
122+
value={nickname as string}
123+
onChange={(e) => setNickname(e.target.value)}
124+
readOnly={!editMode}
125+
className={`p-2 ${!editMode ? 'bg-transparent' : ''}`}
126+
/>
127+
</div>
128+
{/* <div className="flex justify-between items-center">
129+
<span>이메일</span>
130+
<input
131+
type="text"
132+
value={user?.user_name}
133+
placeholder="이메일"
134+
readOnly
135+
className="p-2 bg-transparent"
136+
/>
137+
</div> */}
138+
<div className="flex items-center justify-between">
139+
<span>전화번호</span>
140+
<input type="text" value={user?.phone} readOnly className="bg-transparent p-2" />
141+
</div>
142+
</div>
143+
144+
{editMode && (
145+
<div className="mt-20 flex gap-2">
146+
<button className="btn btn-outline btn-primary flex-1" onClick={() => resetData()}>
147+
취소
148+
</button>
149+
<button type="submit" className="btn btn-outline btn-primary flex-1" onClick={() => updateProfile()}>
150+
수정완료
151+
</button>
152+
</div>
153+
)}
154+
</div>
17155
</div>
18156
);
19157
};

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)