Skip to content

Commit ad30b7d

Browse files
authored
Merge pull request #191 from imaginer-dev/164---ui-프로필-수정-구현
164 UI 프로필 수정 구현
2 parents 52576d6 + 536f40e commit ad30b7d

File tree

6 files changed

+113
-44
lines changed

6 files changed

+113
-44
lines changed

README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,9 @@ Date Leaf를 사용하여 개발하기 위해서는 Node.js가 필요합니다.
5454
## OUR CHALLENGE
5555

5656
## WHO WE ARE
57-
* 이예서
58-
* 이정아
59-
* 김도영
60-
* 한현정
61-
* 최기환
57+
58+
- 이예서
59+
- 이정아
60+
- 김도영
61+
- 한현정
62+
- 최기환

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"@tanstack/react-query": "^5.29.0",
2525
"@vitejs/plugin-react-swc": "^3.5.0",
2626
"axios": "^1.6.8",
27+
"browser-image-compression": "^2.0.2",
2728
"daisyui": "^4.10.2",
2829
"react": "^18.2.0",
2930
"react-dom": "^18.2.0",

pnpm-lock.yaml

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/assets/svgs/ProfileIcon.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const ProfileIcon = ({
44
height = '49', // 기본 세로 크기
55
fill = 'white', // 기본 색상
66
}: {
7-
imageUrl: string | null;
7+
imageUrl: string | null | undefined;
88
width?: string;
99
height?: string;
1010
fill?: string;
@@ -19,8 +19,8 @@ const ProfileIcon = ({
1919
/>
2020
</svg>
2121
) : (
22-
<div className={`avatar h-[49[px]] w-[49px] overflow-hidden rounded-full`}>
23-
<div>
22+
<div className={`avatar overflow-hidden rounded-full`}>
23+
<div style={{ width: width, height: height }}>
2424
<img className="object-cover" src={imageUrl} alt="Profile" />
2525
</div>
2626
</div>

src/components/common/SideBar/SideBarProfile.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const SideBarProfile: FC<Props> = ({ userName, imageUrl }) => {
1616
return (
1717
<div className="flex w-full flex-row justify-between border-b-2 border-white pb-4">
1818
<Link to={`/profile`} className="flex flex-row items-center gap-1 text-lg">
19-
<ProfileIcon imageUrl={imageUrl} />
19+
<ProfileIcon width="49px" height="49px" imageUrl={imageUrl} />
2020
<span className="ml-2">{userName}</span>
2121
</Link>
2222
<div className="flex flex-row items-center gap-2">

src/pages/ProfilePage.tsx

Lines changed: 87 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,111 @@
1-
import { useState, FC, ChangeEvent } from 'react';
1+
import { useState, useRef, FC, ChangeEvent } from 'react';
2+
import imageCompression from 'browser-image-compression';
23
import { useGetProfile } from '@/react-queries/userGetProfile';
34
import HistoryBackButton from '@/components/common/HistoryBackButton';
45
import ProfileIcon from '@/assets/svgs/ProfileIcon';
5-
// import { useMutation } from '@tanstack/react-query';
66
import { updateUserProfile } from '@/apis/updateUserApi';
7+
import { LooseValidation, ValidateProcessor } from '@/utils/authUtils';
8+
import Dialog from '@/components/common/Dialog';
79

810
interface Props {
911
userProfile?: {
1012
imageUrl: string | null;
1113
};
1214
}
1315

16+
interface DialogElement {
17+
openModal: () => void;
18+
closeModal: () => void;
19+
}
20+
1421
const ProfilePage: FC<Props> = ({ userProfile }) => {
1522
const { data: user, error, isLoading, isError } = useGetProfile();
1623
const [editMode, setEditMode] = useState(false);
17-
const [imageUrl, setImageUrl] = useState(userProfile ? userProfile?.imageUrl : null);
24+
const [imageUrl, setImageUrl] = useState(userProfile ? userProfile.imageUrl : user?.image_url);
25+
const [originalImageUrl] = useState(userProfile ? userProfile.imageUrl : user?.image_url);
1826
const [nickname, setNickname] = useState(user ? user?.user_nickname : '');
27+
const [originalNickname] = useState(user ? user.user_nickname : '');
1928
const [selectedFile, setSelectedFile] = useState<File | null>(null);
29+
const [fileName, setFileName] = useState('');
30+
const validator = new ValidateProcessor(new LooseValidation());
31+
const dialogRef = useRef<DialogElement | null>(null);
32+
const [dialogMessage, setDialogMessage] = useState('');
2033

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-
}
34+
const resetProfileStateExceptImage = () => {
35+
setNickname(user ? user.user_nickname : '');
36+
setSelectedFile(null);
37+
setFileName('');
38+
setEditMode(false);
3139
};
3240

41+
// 편집모드
3342
const toggleEditMode = () => {
3443
setEditMode(!editMode);
3544
};
3645

46+
//편집모드 취소
3747
const resetData = () => {
38-
if (user) {
39-
setNickname(user.user_nickname);
40-
setImageUrl(userProfile ? userProfile.imageUrl : null);
41-
setSelectedFile(null);
48+
setImageUrl(userProfile ? userProfile.imageUrl : user?.image_url);
49+
resetProfileStateExceptImage();
50+
};
51+
52+
//이미지 압축
53+
const getImgUpload = async (image: File) => {
54+
const options = {
55+
maxSizeMB: 0.5, // 최대 파일 크기를 0.5MB로 제한
56+
maxWidthOrHeight: 1920, // 최대 너비 또는 높이를 1920px로 제한
57+
useWebWorker: true,
58+
};
59+
const resizingBlob = await imageCompression(image, options);
60+
const resizingFile = new File([resizingBlob], image.name, { type: image.type });
61+
console.log(`원본 파일 크기: ${(image.size / 1024).toFixed(2)} KB`);
62+
console.log(`압축 후 파일 크기: ${(resizingBlob.size / 1024).toFixed(2)} KB`);
63+
return resizingFile;
64+
};
65+
66+
//프로필 이미지 수정
67+
const handleImageChange = async (event: ChangeEvent<HTMLInputElement>) => {
68+
const file = event.target.files ? event.target.files[0] : null;
69+
if (file) {
70+
// 이미지 파일 형식 검사
71+
if (!file.type.startsWith('image/')) {
72+
setDialogMessage('※ 이미지 파일만 업로드 가능합니다.');
73+
dialogRef.current?.openModal();
74+
return; // 함수 종료
75+
}
76+
77+
setFileName(file.name);
78+
79+
try {
80+
const compressedFile = await getImgUpload(file);
81+
const reader = new FileReader();
82+
reader.onloadend = () => {
83+
setImageUrl(reader.result as string);
84+
};
85+
reader.readAsDataURL(compressedFile);
86+
setSelectedFile(compressedFile);
87+
} catch (error) {
88+
console.error('이미지 압축 실패:', error);
89+
}
4290
}
43-
setEditMode(false);
4491
};
4592

93+
//프로필 수정완료
4694
const updateProfile = async () => {
4795
if (!user) return;
4896

97+
// 변경사항 없을 때 알림
98+
if (nickname === originalNickname && imageUrl === originalImageUrl && !selectedFile) {
99+
setDialogMessage('수정된 내용이 없습니다.');
100+
dialogRef.current?.openModal();
101+
return;
102+
}
103+
104+
if (nickname && !validator.isValidNickName(nickname)) {
105+
setDialogMessage('※ 영문, 숫자, 한글만 사용 가능하며, 2~12자 이내여야 합니다.');
106+
dialogRef.current?.openModal();
107+
return;
108+
}
49109
const param = {
50110
id: user.id,
51111
user_nickname: nickname,
@@ -55,9 +115,14 @@ const ProfilePage: FC<Props> = ({ userProfile }) => {
55115
try {
56116
const updatedData = await updateUserProfile(param);
57117
console.log('프로필 업데이트 성공:', updatedData);
58-
setEditMode(false);
118+
setDialogMessage('수정 완료되었습니다.');
119+
dialogRef.current?.openModal();
120+
resetProfileStateExceptImage();
59121
} catch (error) {
60122
console.error('프로필 업데이트 실패:', error);
123+
setDialogMessage('수정 실패되었습니다.');
124+
dialogRef.current?.openModal();
125+
resetProfileStateExceptImage();
61126
}
62127
};
63128

@@ -88,22 +153,18 @@ const ProfilePage: FC<Props> = ({ userProfile }) => {
88153
</nav>
89154
<div className="container mx-auto flex max-w-sm flex-1 flex-col gap-4 pb-[50px] pt-4">
90155
<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> */}
96156
<ProfileIcon imageUrl={imageUrl} width="100%" height="100%" fill="#CCCFC4" />
97157
{editMode && (
98158
<label
99159
htmlFor="img"
100160
className="absolute inset-0 flex flex-col items-center justify-center gap-2.5 rounded-full bg-black bg-opacity-50 text-white"
101161
>
102162
<span>이미지를 드래그해서 넣어주세요!</span>
163+
<span className="ml-2 mr-2 line-clamp-1 text-sm">{fileName && `선택된 파일: ${fileName}`}</span>
103164
<input
104165
type="file"
105166
id="img"
106-
className="file-input file-input-bordered file-input-success file-input-sm w-4/5 max-w-xs"
167+
className="file-input-s file-input file-input-bordered file-input-success w-4/5 max-w-xs"
107168
accept="image/*"
108169
onChange={(e) => handleImageChange(e)}
109170
/>
@@ -125,16 +186,6 @@ const ProfilePage: FC<Props> = ({ userProfile }) => {
125186
className={`p-2 ${!editMode ? 'bg-transparent' : ''}`}
126187
/>
127188
</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> */}
138189
<div className="flex items-center justify-between">
139190
<span>전화번호</span>
140191
<input type="text" value={user?.phone} readOnly className="bg-transparent p-2" />
@@ -152,6 +203,7 @@ const ProfilePage: FC<Props> = ({ userProfile }) => {
152203
</div>
153204
)}
154205
</div>
206+
<Dialog ref={dialogRef} desc={dialogMessage}></Dialog>
155207
</div>
156208
);
157209
};

0 commit comments

Comments
 (0)