1
- import { useState , FC , ChangeEvent } from 'react' ;
1
+ import { useState , useRef , FC , ChangeEvent } from 'react' ;
2
+ import imageCompression from 'browser-image-compression' ;
2
3
import { useGetProfile } from '@/react-queries/userGetProfile' ;
3
4
import HistoryBackButton from '@/components/common/HistoryBackButton' ;
4
5
import ProfileIcon from '@/assets/svgs/ProfileIcon' ;
5
- // import { useMutation } from '@tanstack/react-query';
6
6
import { updateUserProfile } from '@/apis/updateUserApi' ;
7
+ import { LooseValidation , ValidateProcessor } from '@/utils/authUtils' ;
8
+ import Dialog from '@/components/common/Dialog' ;
7
9
8
10
interface Props {
9
11
userProfile ?: {
10
12
imageUrl : string | null ;
11
13
} ;
12
14
}
13
15
16
+ interface DialogElement {
17
+ openModal : ( ) => void ;
18
+ closeModal : ( ) => void ;
19
+ }
20
+
14
21
const ProfilePage : FC < Props > = ( { userProfile } ) => {
15
22
const { data : user , error, isLoading, isError } = useGetProfile ( ) ;
16
23
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 ) ;
18
26
const [ nickname , setNickname ] = useState ( user ? user ?. user_nickname : '' ) ;
27
+ const [ originalNickname ] = useState ( user ? user . user_nickname : '' ) ;
19
28
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 ( '' ) ;
20
33
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 ) ;
31
39
} ;
32
40
41
+ // 편집모드
33
42
const toggleEditMode = ( ) => {
34
43
setEditMode ( ! editMode ) ;
35
44
} ;
36
45
46
+ //편집모드 취소
37
47
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
+ }
42
90
}
43
- setEditMode ( false ) ;
44
91
} ;
45
92
93
+ //프로필 수정완료
46
94
const updateProfile = async ( ) => {
47
95
if ( ! user ) return ;
48
96
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
+ }
49
109
const param = {
50
110
id : user . id ,
51
111
user_nickname : nickname ,
@@ -55,9 +115,14 @@ const ProfilePage: FC<Props> = ({ userProfile }) => {
55
115
try {
56
116
const updatedData = await updateUserProfile ( param ) ;
57
117
console . log ( '프로필 업데이트 성공:' , updatedData ) ;
58
- setEditMode ( false ) ;
118
+ setDialogMessage ( '수정 완료되었습니다.' ) ;
119
+ dialogRef . current ?. openModal ( ) ;
120
+ resetProfileStateExceptImage ( ) ;
59
121
} catch ( error ) {
60
122
console . error ( '프로필 업데이트 실패:' , error ) ;
123
+ setDialogMessage ( '수정 실패되었습니다.' ) ;
124
+ dialogRef . current ?. openModal ( ) ;
125
+ resetProfileStateExceptImage ( ) ;
61
126
}
62
127
} ;
63
128
@@ -88,22 +153,18 @@ const ProfilePage: FC<Props> = ({ userProfile }) => {
88
153
</ nav >
89
154
< div className = "container mx-auto flex max-w-sm flex-1 flex-col gap-4 pb-[50px] pt-4" >
90
155
< 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
156
< ProfileIcon imageUrl = { imageUrl } width = "100%" height = "100%" fill = "#CCCFC4" />
97
157
{ editMode && (
98
158
< label
99
159
htmlFor = "img"
100
160
className = "absolute inset-0 flex flex-col items-center justify-center gap-2.5 rounded-full bg-black bg-opacity-50 text-white"
101
161
>
102
162
< span > 이미지를 드래그해서 넣어주세요!</ span >
163
+ < span className = "ml-2 mr-2 line-clamp-1 text-sm" > { fileName && `선택된 파일: ${ fileName } ` } </ span >
103
164
< input
104
165
type = "file"
105
166
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"
107
168
accept = "image/*"
108
169
onChange = { ( e ) => handleImageChange ( e ) }
109
170
/>
@@ -125,16 +186,6 @@ const ProfilePage: FC<Props> = ({ userProfile }) => {
125
186
className = { `p-2 ${ ! editMode ? 'bg-transparent' : '' } ` }
126
187
/>
127
188
</ 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
189
< div className = "flex items-center justify-between" >
139
190
< span > 전화번호</ span >
140
191
< input type = "text" value = { user ?. phone } readOnly className = "bg-transparent p-2" />
@@ -152,6 +203,7 @@ const ProfilePage: FC<Props> = ({ userProfile }) => {
152
203
</ div >
153
204
) }
154
205
</ div >
206
+ < Dialog ref = { dialogRef } desc = { dialogMessage } > </ Dialog >
155
207
</ div >
156
208
) ;
157
209
} ;
0 commit comments