-
Notifications
You must be signed in to change notification settings - Fork 4
[♻️ Refactor/188] 프로필 수정 범위 제한 및 프로필 이미지 변경 로직 추가 #210
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| 'use client'; | ||
|
|
||
| import { createContext, useContext, ReactNode } from 'react'; | ||
|
|
||
| interface ProfileImageContextValue { | ||
| profileImage: File | null; | ||
| profileImagePreview: string | null; | ||
| handleImageChange: (file: File) => void; | ||
| resetImage: () => void; | ||
| updatePreview: (url: string) => void; | ||
| } | ||
|
|
||
| const ProfileImageContext = createContext<ProfileImageContextValue | null>( | ||
| null | ||
| ); | ||
|
|
||
| export function ProfileImageProvider({ | ||
| children, | ||
| value, | ||
| }: { | ||
| children: ReactNode; | ||
| value: ProfileImageContextValue; | ||
| }) { | ||
| return ( | ||
| <ProfileImageContext.Provider value={value}> | ||
| {children} | ||
| </ProfileImageContext.Provider> | ||
| ); | ||
| } | ||
|
|
||
| export function useProfileImageContext() { | ||
| const context = useContext(ProfileImageContext); | ||
| if (!context) { | ||
| throw new Error( | ||
| 'useProfileImageContext must be used within ProfileImageProvider' | ||
| ); | ||
| } | ||
| return context; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,129 @@ | ||
| import { useRouter } from 'next/navigation'; | ||
| import { useCallback } from 'react'; | ||
|
|
||
| import { ApiError } from '@/config/client'; | ||
|
|
||
| /** | ||
| * 에러 처리 Hook | ||
| * - API 에러 핸들링 | ||
| * - 401 Unauthorized 시 로그인 페이지 리다이렉트 | ||
| * - 사용자 친화적 에러 메시지 표시 | ||
| */ | ||
| export function useErrorHandler() { | ||
| const router = useRouter(); | ||
|
|
||
| /** | ||
| * 일반 에러 처리 | ||
| */ | ||
| const handleError = useCallback( | ||
| (error: unknown, fallbackMessage = '오류가 발생했습니다.') => { | ||
| console.error('Error:', error); | ||
|
|
||
| if (error instanceof ApiError) { | ||
| // 401 Unauthorized - 로그인 페이지로 리다이렉트 | ||
| if (error.status === 401) { | ||
| alert('로그인이 필요합니다.'); | ||
| localStorage.removeItem('accessToken'); | ||
| router.push('/login'); | ||
| return; | ||
| } | ||
|
|
||
| // 403 Forbidden | ||
| if (error.status === 403) { | ||
| alert('접근 권한이 없습니다.'); | ||
| return; | ||
| } | ||
|
|
||
| // 404 Not Found | ||
| if (error.status === 404) { | ||
| alert('요청한 리소스를 찾을 수 없습니다.'); | ||
| return; | ||
| } | ||
|
|
||
| // 409 Conflict | ||
| if (error.status === 409) { | ||
| alert(error.message || '이미 존재하는 데이터입니다.'); | ||
| return; | ||
| } | ||
|
|
||
| // 422 Validation Error | ||
| if (error.status === 422) { | ||
| alert(error.message || '입력 데이터를 확인해주세요.'); | ||
| return; | ||
| } | ||
|
|
||
| // 500 Internal Server Error | ||
| if (error.status >= 500) { | ||
| alert('서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.'); | ||
| return; | ||
| } | ||
|
|
||
| // 기타 API 에러 | ||
| alert(error.message || fallbackMessage); | ||
| } else if (error instanceof Error) { | ||
| // 일반 JavaScript 에러 | ||
| alert(error.message || fallbackMessage); | ||
| } else { | ||
| // 알 수 없는 에러 | ||
| alert(fallbackMessage); | ||
| } | ||
| }, | ||
| [router] | ||
| ); | ||
|
|
||
| /** | ||
| * 이미지 업로드 에러 처리 | ||
| */ | ||
| const handleImageUploadError = useCallback( | ||
| (error: unknown) => { | ||
| console.error('Image upload error:', error); | ||
|
|
||
| if (error instanceof ApiError) { | ||
| if (error.status === 413) { | ||
| alert('이미지 파일 크기가 너무 큽니다. (최대 4MB)'); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 프로필 편집 컴포넌트에 등록된 사이즈 제한과 통일하거나 연결해서 재사용하면 좋을거 같아요 |
||
| return; | ||
| } | ||
|
|
||
| if (error.status === 415) { | ||
| alert('지원하지 않는 이미지 형식입니다. (JPG, PNG, GIF만 가능)'); | ||
| return; | ||
| } | ||
| } | ||
|
|
||
| handleError(error, '이미지 업로드에 실패했습니다.'); | ||
| }, | ||
| [handleError] | ||
| ); | ||
|
|
||
| /** | ||
| * 프로필 업데이트 에러 처리 | ||
| */ | ||
| const handleProfileUpdateError = useCallback( | ||
| (error: unknown) => { | ||
| console.error('Profile update error:', error); | ||
|
|
||
| if (error instanceof ApiError) { | ||
| // 닉네임 중복 | ||
| if (error.status === 409) { | ||
| alert('이미 사용 중인 닉네임입니다.'); | ||
| return; | ||
| } | ||
|
|
||
| // 비밀번호 형식 오류 | ||
| if (error.status === 400 && error.message?.includes('password')) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 에러 메시지 문자열( 대신 백엔드에서 에러 코드나 에러가 발생한 필드명을 포함한 구조화된 에러 응답을 보내주도록 협의하고, 이를 기반으로 분기하는 것이 더 안정적인 방법입니다. 예를 들어, References
|
||
| alert('비밀번호는 8자 이상이어야 합니다.'); | ||
| return; | ||
| } | ||
| } | ||
|
|
||
| handleError(error, '프로필 수정에 실패했습니다.'); | ||
| }, | ||
| [handleError] | ||
| ); | ||
|
|
||
| return { | ||
| handleError, | ||
| handleImageUploadError, | ||
| handleProfileUpdateError, | ||
| }; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
localStorage에accessToken을 저장하고 관리하는 것은 XSS(Cross-Site Scripting) 공격에 취약할 수 있습니다.localStorage는 JavaScript로 쉽게 접근할 수 있기 때문입니다.보안 강화를 위해
accessToken은 변수와 같이 메모리에 저장하고, 리프레시 토큰은httpOnly쿠키에 저장하는 방식을 고려해 보세요. 이는 스크립트를 통한 토큰 탈취 위험을 크게 줄여줍니다.