1- import React , { useState , useEffect } from 'react' ;
1+ import React , { useState , useEffect , useRef } from 'react' ;
22
33// 쿠키에서 값을 읽어오는 함수
44const getCookieValue = ( name : string ) : string => {
@@ -16,7 +16,7 @@ const getCookieValue = (name: string): string => {
1616type MembershipType = 'BASIC' | 'VIP' ;
1717
1818const Profile : React . FC = ( ) => {
19- const [ userInfo , setUserInfo ] = useState ( { name : '' , email : '' , userid : '' } ) ;
19+ const [ userInfo , setUserInfo ] = useState ( { name : '' , email : '' , userid : '' , profileImage : '' } ) ;
2020 const [ editedInfo , setEditedInfo ] = useState ( { name : '' , email : '' , userid : '' , pw : '' } ) ;
2121 const [ membership , setMembership ] = useState < MembershipType > ( 'BASIC' ) ;
2222 const [ isLoading , setIsLoading ] = useState ( true ) ;
@@ -26,6 +26,10 @@ const Profile: React.FC = () => {
2626 const [ isEmailSent , setIsEmailSent ] = useState ( false ) ;
2727 const [ emailVerifyStatus , setEmailVerifyStatus ] = useState < 'idle' | 'success' | 'error' | 'expired' | 'notfound' > ( 'idle' ) ;
2828 const [ emailVerifyMessage , setEmailVerifyMessage ] = useState ( '' ) ;
29+ const [ profileImageUrl , setProfileImageUrl ] = useState < string | null > ( null ) ;
30+ const [ profileImagePreview , setProfileImagePreview ] = useState < string | null > ( null ) ;
31+ const [ uploading , setUploading ] = useState ( false ) ;
32+ const fileInputRef = useRef < HTMLInputElement > ( null ) ;
2933
3034 useEffect ( ( ) => {
3135 // 사용자 정보와 멤버십 정보 모두 가져오기
@@ -81,8 +85,11 @@ const Profile: React.FC = () => {
8185
8286 const data = await response . json ( ) ;
8387 console . log ( 'User Info Fetched:' , data ) ;
84- setUserInfo ( data ) ;
88+ setUserInfo ( { ... data , profileImage : data . profileImage || data . image || '' } ) ;
8589 setEditedInfo ( { ...data , pw : '' } ) ; // data에 userid 포함
90+ // 프로필 이미지 URL 세팅 (백엔드에서 profileImage 필드로 내려줘야 함)
91+ setProfileImageUrl ( data . profileImage || null ) ;
92+ setProfileImagePreview ( null ) ; // 새로고침 시 미리보기 초기화
8693
8794 // DB의 users 테이블에 membership 필드가 있으므로 이 정보를 사용
8895 if ( data . membership ) {
@@ -96,6 +103,59 @@ const Profile: React.FC = () => {
96103 }
97104 } ;
98105
106+
107+ const handleProfileImageClick = ( ) => {
108+ if ( fileInputRef . current ) fileInputRef . current . click ( ) ;
109+ } ;
110+
111+ // 파일 선택 시 미리보기 및 업로드
112+ const handleProfileImageChange = async ( e : React . ChangeEvent < HTMLInputElement > ) => {
113+ if ( ! e . target . files || e . target . files . length === 0 ) return ;
114+ const file = e . target . files [ 0 ] ;
115+
116+ // 파일 크기 제한 (예: 5MB)
117+ const maxSize = 5 * 1024 * 1024 ;
118+ if ( file . size > maxSize ) {
119+ alert ( '파일 크기가 5MB를 초과할 수 없습니다.' ) ;
120+ return ;
121+ }
122+
123+ // 미리보기
124+ const reader = new FileReader ( ) ;
125+ reader . onloadend = ( ) => setProfileImagePreview ( reader . result as string ) ;
126+ reader . readAsDataURL ( file ) ;
127+
128+ // 업로드
129+ try {
130+ setUploading ( true ) ;
131+ const jwtToken = getCookieValue ( 'jwt-token' ) ;
132+ const formData = new FormData ( ) ;
133+ formData . append ( 'file' , file ) ;
134+
135+ const res = await fetch ( '/server/user/profile/image' , {
136+ method : 'POST' ,
137+ headers : {
138+ 'Authorization' : jwtToken || ''
139+ } ,
140+ body : formData ,
141+ } ) ;
142+ const data = await res . json ( ) ;
143+ if ( res . ok && data . url ) {
144+ setProfileImageUrl ( data . url ) ;
145+ setProfileImagePreview ( null ) ;
146+ // userInfo에도 반영
147+ setUserInfo ( prev => ( { ...prev , profileImage : data . url } ) ) ;
148+ alert ( '프로필 이미지가 성공적으로 업로드되었습니다.' ) ;
149+ } else {
150+ alert ( data . message || '프로필 이미지 업로드에 실패했습니다.' ) ;
151+ }
152+ } catch ( err ) {
153+ alert ( '프로필 이미지 업로드 중 오류가 발생했습니다.' ) ;
154+ } finally {
155+ setUploading ( false ) ;
156+ }
157+ } ;
158+
99159 const handleUpdate = async ( ) => {
100160 try {
101161 console . log ( 'Updating User Info:' , editedInfo ) ;
@@ -244,9 +304,41 @@ const Profile: React.FC = () => {
244304 < div className = "bg-[#2a2928] rounded-lg p-8 shadow-lg" >
245305 { /* 프로필 상단 영역: 이미지와 멤버십 배지 */ }
246306 < div className = "flex flex-col items-center mb-8" >
247- { /* 프로필 이미지 */ }
248- < div className = "w-32 h-32 bg-[#3f3f3f] rounded-full flex items-center justify-center shadow-md mb-4 border-2 border-[#3b7cc9]" >
249- < span className = "text-6xl" > 👤</ span >
307+ < div
308+ className = "w-32 h-32 bg-[#3f3f3f] rounded-full flex items-center justify-center shadow-md mb-4 border-2 border-[#3b7cc9] cursor-pointer relative overflow-hidden"
309+ title = "프로필 이미지 업로드"
310+ onClick = { handleProfileImageClick }
311+ style = { { position : 'relative' } }
312+ >
313+ { profileImagePreview ? (
314+ < img
315+ src = { profileImagePreview }
316+ alt = "프로필 미리보기"
317+ className = "w-full h-full object-cover rounded-full"
318+ />
319+ ) : userInfo . profileImage ? (
320+ < img
321+ src = { userInfo . profileImage }
322+ alt = "프로필"
323+ className = "w-full h-full object-cover rounded-full"
324+ />
325+ ) : (
326+ < span className = "text-6xl" > 👤</ span >
327+ ) }
328+ { uploading && (
329+ < div className = "absolute inset-0 bg-black bg-opacity-40 flex items-center justify-center" >
330+ < div className = "animate-spin rounded-full h-10 w-10 border-t-2 border-b-2 border-[#3b7cc9]" > </ div >
331+ </ div >
332+ ) }
333+ < input
334+ type = "file"
335+ accept = "image/png,image/jpeg"
336+ ref = { fileInputRef }
337+ style = { { display : 'none' } }
338+ onChange = { handleProfileImageChange }
339+ aria-label = "프로필 이미지 업로드"
340+ />
341+ < span className = "absolute bottom-2 right-2 bg-[#3b7cc9] text-white text-xs px-2 py-1 rounded shadow" > 변경</ span >
250342 </ div >
251343
252344 { /* 멤버십 배지 */ }
0 commit comments