-
Notifications
You must be signed in to change notification settings - Fork 2
✨Feat: 프로필 수정 컴포넌트 구현 #78
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
Conversation
|
""" Walkthrough마이페이지(프로필) 기능을 위한 API, 타입, 훅, 컴포넌트, 검증 스키마 등이 새롭게 추가되었습니다. 기존 인증 및 대시보드 관련 API 호출 코드들은 공통 axios 인스턴스(authHttpClient)로 일원화되었으며, 마이페이지 UI는 ProfileEditForm 컴포넌트로 리팩터링되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant ProfileEditForm
participant useUserQuery
participant useUploadProfileImageMutation
participant useUpdateMyProfileMutation
participant mypageApi
User->>ProfileEditForm: 페이지 진입
ProfileEditForm->>useUserQuery: 사용자 정보 조회
useUserQuery->>mypageApi: loadUser()
mypageApi-->>useUserQuery: 사용자 데이터 반환
useUserQuery-->>ProfileEditForm: 데이터 제공
User->>ProfileEditForm: 프로필 이미지/닉네임 수정, 저장 클릭
alt 이미지 변경
ProfileEditForm->>useUploadProfileImageMutation: 이미지 업로드
useUploadProfileImageMutation->>mypageApi: uploadProfileImage(file)
mypageApi-->>useUploadProfileImageMutation: profileImageUrl 반환
useUploadProfileImageMutation-->>ProfileEditForm: profileImageUrl 전달
end
ProfileEditForm->>useUpdateMyProfileMutation: 닉네임/이미지 URL로 프로필 수정
useUpdateMyProfileMutation->>mypageApi: updateMyProfile(data)
mypageApi-->>useUpdateMyProfileMutation: 수정된 사용자 데이터 반환
useUpdateMyProfileMutation-->>ProfileEditForm: 성공/실패 알림
Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
npm error Exit handler never called! 📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (4)
✅ Files skipped from review due to trivial changes (2)
🚧 Files skipped from review as they are similar to previous changes (1)
🧰 Additional context used🧬 Code Graph Analysis (1)src/app/features/mypage/components/ProfileImageUpload.tsx (3)
🪛 Biome (1.9.4)src/app/features/mypage/components/ProfileImageUpload.tsx[error] 55-56: Change to an optional chain. Unsafe fix: Change to an optional chain. (lint/complexity/useOptionalChain) 🔇 Additional comments (4)
✨ Finishing Touches
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
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.
Actionable comments posted: 3
🔭 Outside diff range comments (1)
src/app/features/mypage/hook/useUserQurey.ts (1)
1-11: 파일명에 오타가 있습니다.파일명이
useUserQurey.ts로 되어 있는데,useUserQuery.ts로 수정해야 합니다. "Query"가 올바른 철자입니다.그 외에는 React Query를 사용한 커스텀 훅 구현이 적절합니다.
🧹 Nitpick comments (6)
src/app/shared/components/common/WhitePenIcon/WhitePenIcon.tsx (1)
1-20: SVG 접근성 개선 제안:aria-hidden="true"추가 고려
현재 SVG는 스크린 리더에서 decorative 아이콘으로 처리되지 않을 수 있습니다.<svg width={size} height={size} viewBox="0 0 24 24" fill="none" + role="img" + aria-hidden="true" {...props} >src/app/shared/components/common/PlusIcon/PlusIcon.tsx (1)
1-38: SVG 접근성 개선 제안:aria-hidden="true"추가 고려
PlusIcon도 decorative 아이콘인 경우 스크린 리더 무시를 위해aria-hidden="true"를 추가하면 좋습니다.<svg className={className} width={size} height={size} viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg" + role="img" + aria-hidden="true" {...props} >src/app/shared/components/Input.tsx (1)
48-49: 중복된readOnly조건부 클래스 분리 제안
password/기본 input 분기에서 동일한props.readOnly && 'Text-gray cursor-default'로직이 중복됩니다.
readOnly를 구조 분해로 추출하고, 공통readOnlyClass변수를 정의해 중복을 줄이면 가독성과 유지보수성이 향상됩니다.Also applies to: 76-77
src/app/mypage/page.tsx (1)
35-37: 버튼 구현이 개선되었습니다.
type="button"명시와 멀티라인 포맷팅이 좋습니다. 접근성을 위해aria-label을 추가하는 것을 고려해보세요.- <button type="button" className="text-xm self-start"> + <button type="button" className="text-xm self-start" aria-label="이전 페이지로 돌아가기"> 돌아가기 </button>src/app/shared/components/common/CloseCircleIcon/CloseCircleIcon.tsx (1)
10-11: 불필요한 Fragment를 제거하세요.SVG 요소 하나만 반환하므로 Fragment가 필요하지 않습니다.
- <> <svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props} > {/* ... */} </svg> - </>Also applies to: 38-39
src/app/features/mypage/components/ProfileImageUpload.tsx (1)
28-42: 파일 검증 로직 추가를 고려해 주세요.현재 구현은 기본적인 파일 처리를 잘 하고 있지만, 사용자 경험 향상을 위해 파일 검증을 추가하는 것을 제안합니다.
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { const file = e.target.files?.[0] if (!file) return // 파일 크기 검증 (예: 5MB 제한) if (file.size > 5 * 1024 * 1024) { showError('파일 크기는 5MB 이하로 업로드해 주세요.') return } // 파일 타입 검증 if (!file.type.startsWith('image/')) { showError('이미지 파일만 업로드할 수 있습니다.') return } const url = URL.createObjectURL(file) // ... 기존 로직 }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (19)
src/app/features/auth/api/authApi.ts(1 hunks)src/app/features/mypage/api/mypageApi.ts(1 hunks)src/app/features/mypage/api/mypageEndPoint.ts(1 hunks)src/app/features/mypage/components/ProfileEditForm.tsx(1 hunks)src/app/features/mypage/components/ProfileImageUpload.tsx(1 hunks)src/app/features/mypage/hook/useUpdateMyProfileMutation.ts(1 hunks)src/app/features/mypage/hook/useUploadProfileImageMutation.ts(1 hunks)src/app/features/mypage/hook/useUserQurey.ts(1 hunks)src/app/features/mypage/schemas/mypageValidation.ts(1 hunks)src/app/features/mypage/types/mypage.type.ts(1 hunks)src/app/globals.css(1 hunks)src/app/mypage/page.tsx(3 hunks)src/app/shared/components/Input.tsx(2 hunks)src/app/shared/components/common/CloseCircleIcon/CloseCircleIcon.tsx(1 hunks)src/app/shared/components/common/PlusIcon/PlusIcon.tsx(1 hunks)src/app/shared/components/common/WhitePenIcon/WhitePenIcon.tsx(1 hunks)src/app/shared/components/common/sidebar/modal/CreateDashboardModal.tsx(2 hunks)src/app/shared/hooks/useDashboard.ts(2 hunks)src/app/shared/lib/axios.ts(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (8)
src/app/shared/hooks/useDashboard.ts (1)
src/app/shared/types/dashboard.ts (1)
DashboardListResponse(13-17)
src/app/features/auth/api/authApi.ts (2)
src/app/features/auth/types/auth.type.ts (3)
LoginRequest(3-6)LoginResponse(8-11)SignupRequest(13-17)src/app/features/auth/api/authEndpoint.ts (1)
AUTH_ENDPOINT(1-4)
src/app/shared/lib/axios.ts (1)
src/app/features/auth/store/useAuthStore.ts (1)
useAuthStore(5-20)
src/app/features/mypage/hook/useUserQurey.ts (1)
src/app/features/mypage/api/mypageApi.ts (1)
loadUser(11-14)
src/app/features/mypage/hook/useUploadProfileImageMutation.ts (2)
src/app/features/mypage/types/mypage.type.ts (1)
UploadProfileImageResponse(6-8)src/app/features/mypage/api/mypageApi.ts (1)
uploadProfileImage(26-36)
src/app/features/mypage/hook/useUpdateMyProfileMutation.ts (2)
src/app/features/mypage/types/mypage.type.ts (1)
UpdateProfileRequest(1-4)src/app/features/mypage/api/mypageApi.ts (1)
updateMyProfile(16-24)
src/app/features/mypage/api/mypageApi.ts (2)
src/app/features/mypage/api/mypageEndPoint.ts (1)
MYPAGE_ENDPOINT(1-4)src/app/features/mypage/types/mypage.type.ts (2)
UpdateProfileRequest(1-4)UploadProfileImageResponse(6-8)
src/app/features/mypage/components/ProfileImageUpload.tsx (3)
src/app/shared/components/common/WhitePenIcon/WhitePenIcon.tsx (1)
WhitePenIcon(5-20)src/app/shared/components/common/PlusIcon/PlusIcon.tsx (1)
PlusIcon(6-38)src/app/shared/components/common/CloseCircleIcon/CloseCircleIcon.tsx (1)
CloseCircleIcon(5-40)
🔇 Additional comments (23)
src/app/globals.css (1)
81-83: 새로운 유틸리티 클래스 추가가 적절합니다기존 패턴과 일치하며 라이트/다크 모드를 모두 고려한 색상 정의가 잘 되어 있습니다.
src/app/shared/lib/axios.ts (2)
5-7: axios 인스턴스 이름 변경이 적절합니다
authHttpClient라는 명시적인 이름으로 변경하여 인증이 필요한 HTTP 클라이언트임을 명확히 표현했습니다.
9-15:authHttpClient사용처와 로그인/회원가입 훅이 어떤 axios 인스턴스를 쓰는지 확인이 필요합니다. 아래 스크립트를 실행해 주세요.#!/bin/bash echo "=== authHttpClient 사용 검색 ===" rg -n "authHttpClient" echo "=== shared/lib/axios import 검색 ===" rg -n "@\/app\/shared\/lib\/axios" echo "=== useSignupMutation.ts 내용 확인 ===" sed -n '1,200p' src/app/features/auth/hooks/useSignupMutation.ts || echo "useSignupMutation.ts: 파일을 찾을 수 없습니다." echo "=== useLoginMutation.ts 내용 확인 ===" sed -n '1,200p' src/app/features/auth/hooks/useLoginMutation.ts || echo "useLoginMutation.ts: 파일을 찾을 수 없습니다."src/app/shared/hooks/useDashboard.ts (2)
5-5: HTTP 클라이언트 import 업데이트가 적절합니다
authHttpClient로의 변경이 일관되게 적용되었습니다.
24-26: API 호출 업데이트가 올바릅니다대시보드 데이터는 인증이 필요한 리소스이므로
authHttpClient사용이 적절합니다.src/app/shared/components/common/sidebar/modal/CreateDashboardModal.tsx (2)
7-7: HTTP 클라이언트 import 업데이트가 적절합니다
authHttpClient로의 변경이 일관되게 적용되었습니다.
49-52: API 호출 업데이트가 올바릅니다대시보드 생성은 인증이 필요한 작업이므로
authHttpClient사용이 적절합니다.src/app/features/auth/api/authApi.ts (3)
1-1: HTTP 클라이언트 import 업데이트가 적절합니다
authHttpClient로의 변경이 일관되게 적용되었습니다.
8-11: 로그인 API 호출 업데이트 확인
authHttpClient사용으로 변경되었습니다. 로그인 요청 시에는 토큰이 없으므로 인터셉터에서 토큰이 첨부되지 않을 것으로 예상되지만, 동작을 확인해 주세요.
16-19: 회원가입 API 호출 업데이트 확인
authHttpClient사용으로 변경되었습니다. 회원가입 요청 시에도 토큰이 없으므로 문제없을 것으로 예상되지만, 로그인과 마찬가지로 동작을 확인해 주세요.src/app/features/mypage/types/mypage.type.ts (1)
1-8: 타입 정의 적절
UpdateProfileRequest와UploadProfileImageResponse인터페이스가 명확하며, 프로필 이미지 null 허용도 일관성 있습니다.src/app/features/mypage/api/mypageEndPoint.ts (1)
1-4: 환경 변수 존재 여부 확인 필요
process.env.NEXT_PUBLIC_TEAM_ID가 정의되지 않으면 URL이/undefined/...가 됩니다.
빌드/런타임 시 환경 변수가 항상 설정되어 있는지 검증하거나 기본값을 고려하세요.src/app/features/mypage/schemas/mypageValidation.ts (1)
1-8: 검증 스키마가 적절하게 구현되었습니다.정규표현식이 한글과 영문을 올바르게 매칭하고, 길이 제한도 적절합니다. 에러 메시지도 명확합니다.
src/app/mypage/page.tsx (1)
6-6: 컴포넌트 분리가 잘 이루어졌습니다.ProfileEditForm 컴포넌트로 분리하여 관심사를 잘 분리했습니다.
src/app/features/mypage/hook/useUploadProfileImageMutation.ts (1)
7-11: React Query 뮤테이션 훅이 올바르게 구현되었습니다.타입 정의가 명확하고, 에러 처리도 적절합니다. AxiosError와 일반 Error를 모두 포함한 것이 좋습니다.
src/app/features/mypage/hook/useUpdateMyProfileMutation.ts (1)
9-17: 깔끔한 뮤테이션 훅 구현입니다.React Query의
useMutation을 적절히 래핑하여 타입 안전성을 보장하고 있습니다. 제네릭 타입 정의가 정확하며, 구현이 간결합니다.src/app/features/mypage/api/mypageApi.ts (2)
11-24: API 함수들이 잘 구현되어 있습니다.사용자 데이터 로드와 프로필 업데이트 함수가 일관된 패턴으로 구현되어 있고, 타입 안전성이 보장되고 있습니다.
26-36: 이미지 업로드 구현이 적절합니다.FormData를 사용한 파일 업로드 구현이 올바르게 되어 있습니다. 다만 FormData의 키 이름('image')이 백엔드 API 스펙과 일치하는지 확인하시기 바랍니다.
백엔드 API 스펙에서 이미지 업로드 시 사용하는 필드명이 'image'인지 확인해 주세요.
src/app/features/mypage/components/ProfileEditForm.tsx (3)
22-53: 폼 초기화 로직이 잘 구현되어 있습니다.비동기 사용자 데이터 로딩에 대응하여
useEffect와reset을 활용한 폼 초기화가 적절합니다. SSR 도입을 고려한 주석도 좋습니다.
55-77: 폼 제출 로직이 체계적으로 구현되어 있습니다.이미지 업로드와 프로필 업데이트를 순차적으로 처리하는 로직이 명확하고, 주석을 통해 각 단계가 잘 설명되어 있습니다.
92-133: 폼 UI 구성이 깔끔하게 되어 있습니다.Controller를 사용한 커스텀 컴포넌트 연동과 반응형 레이아웃이 잘 구현되어 있습니다.
src/app/features/mypage/components/ProfileImageUpload.tsx (2)
20-42: 파일 처리 로직이 잘 구현되어 있습니다.
URL.createObjectURL을 사용한 미리보기 생성과 파일 전달 로직이 깔끔하게 구현되어 있습니다. 같은 파일 재선택을 위한 input 초기화도 좋은 접근입니다.
52-99: UI 구성이 사용자 친화적으로 잘 구현되어 있습니다.접근성을 고려한 label/input 연결, hover 상태의 시각적 피드백, 그리고 삭제 버튼 배치가 매우 좋습니다. Image 컴포넌트 사용도 적절합니다.
| // 이미지 삭제 처리 | ||
| const handleDelete = () => { | ||
| if (preview) URL.revokeObjectURL(preview) // 메모리 누수 방지 | ||
| setPreview(null) | ||
| onChange(null) // RHF 상태 초기화 | ||
| if (inputRef.current) inputRef.current.value = '' | ||
| } |
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.
🛠️ Refactor suggestion
메모리 누수 방지 로직을 개선해 주세요.
메모리 누수 방지를 위한 URL.revokeObjectURL 호출이 좋습니다만, 더 안전한 구현을 위해 개선을 제안합니다.
const handleDelete = () => {
- if (preview) URL.revokeObjectURL(preview)
+ if (preview && preview.startsWith('blob:')) {
+ URL.revokeObjectURL(preview)
+ }
setPreview(null)
onChange(null)
if (inputRef.current) inputRef.current.value = ''
}또한 컴포넌트 언마운트 시에도 메모리 정리를 위해 useEffect 클린업을 추가하는 것을 고려해 보세요.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // 이미지 삭제 처리 | |
| const handleDelete = () => { | |
| if (preview) URL.revokeObjectURL(preview) // 메모리 누수 방지 | |
| setPreview(null) | |
| onChange(null) // RHF 상태 초기화 | |
| if (inputRef.current) inputRef.current.value = '' | |
| } | |
| // 이미지 삭제 처리 | |
| const handleDelete = () => { | |
| if (preview && preview.startsWith('blob:')) { | |
| URL.revokeObjectURL(preview) | |
| } | |
| setPreview(null) | |
| onChange(null) | |
| if (inputRef.current) inputRef.current.value = '' | |
| } |
🤖 Prompt for AI Agents
In src/app/features/mypage/components/ProfileImageUpload.tsx around lines 44 to
50, improve the memory leak prevention by ensuring URL.revokeObjectURL is called
safely only if preview exists and is a valid object URL. Additionally, add a
useEffect hook with a cleanup function that revokes the object URL when the
component unmounts to prevent memory leaks from lingering URLs.
LeeCh0129
left a comment
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.
프로필 수정 고생많으셨습니다~👍 남은 기한동안도 화이팅입니다 :)
| } | ||
|
|
||
| const response = await api.post(`/${process.env.NEXT_PUBLIC_TEAM_ID}/dashboards`, formData) | ||
| const response = await authHttpClient.post( |
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.
수정하시면서 해당 부분도 함께 수정해주셨군요. 감사합니다~
dkslel1225
left a comment
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.
프로필 수정 컴포넌트 구현 수고하셨습니다!
yuj2n
left a comment
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.
인성님 프로필 수정 컴포넌트 구현 수고많으셨습니다~
수정 내용 반영하여 작업하도록 하겠습니당!!
| showSuccess('프로필 변경이 완료되었습니다.') | ||
| router.refresh() | ||
| } catch (error) { | ||
| if (isAxiosError(error)) { |
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.
axiosError 처리 사용해주셨군용!
| const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { | ||
| const file = e.target.files?.[0] | ||
| if (!file) return | ||
|
|
||
| const url = URL.createObjectURL(file) // 로컬 미리보기 URL 생성 | ||
| setPreview(url) | ||
| onChange(url) // RHF 상태 업데이트 | ||
|
|
||
| // 서버 업로드용 파일을 상위로 전달 | ||
| if (onFileChange) onFileChange(file) | ||
|
|
||
| // 같은 파일 다시 선택할 수 있도록 초기화 | ||
| if (inputRef.current) inputRef.current.value = '' | ||
| } | ||
|
|
||
| // 이미지 삭제 처리 | ||
| const handleDelete = () => { | ||
| if (preview && preview.startsWith('blob:')) { | ||
| URL.revokeObjectURL(preview) // 메모리 누수 방지 | ||
| } | ||
| setPreview(null) | ||
| onChange(null) // RHF 상태 초기화 | ||
| if (inputRef.current) inputRef.current.value = '' | ||
| } |
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.
function으로 함수명 통합하기로 하였던 것으로 기억하는데 const로 선언해주신 이유가 있으실까요?
| import { useAuthStore } from '@/app/features/auth/store/useAuthStore' | ||
|
|
||
| const api = axios.create({ | ||
| const authHttpClient = axios.create({ |
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.
api가 authHttpClient로 수정되었군요! 참고해서 수정하도록 하겠습니다~
| const isPulicPath = publicPaths.some((path) => config.url?.includes(path)) | ||
|
|
||
| if (!isPulicPath && token) { | ||
| if (token) { |
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.
이 부분이 토큰이 안 넘겨지고 있었던 이유였군용~!😮
📌 변경 사항 개요
✨ 요약
인스턴스 관련
프로필 수정 컴포넌트 구현
여러 SVG 아이콘 컴포넌트 추가 (Plus, pen, x 등)
📝 상세 내용
인스턴스 관련
api에서authHttpClient로 변경되었습니다. (적용하고 있던 부분도 반영 완료)컴포넌트 관련
SVG로 작성한 컴포넌트가 추가 되었습니다. 적용과 관련된 부분은 해당 코드를 참고해주세요!ProfileEditForm,ProfileImageUpload컴포넌트가 추가 되었습니다.ProfileEditForm
useUserQuery + reset를 통해 데이터를 최신화 (SSR 도입 시 변경될 수도 있습니다.)reset을 사용하여 유저가 로딩된 시점에서 해당 값을 초기화 하여 폼에 반영했습니다.ref사용하고 있어...register로 제어가 어렵기 때문에Controller사용<Controller ... />name="profileImageUrl"control={control}render={({ field }) => ...}field.value→value={value}field.onChange→onChange={...}onFileChange={(file) => ...}ProfileImageUpload
🔗 관련 이슈
#74
🖼️ 스크린샷
2025-06-19.04-15-39.mp4
호출 관련
/me,/imageAPI 호출 후 새로 고침/me가 많이 보이는 이유는GET요청의/me와PUT요청의/me가 존재합니다.✅ 체크리스트
💡 참고 사항
Summary by CodeRabbit
신규 기능
버그 수정
스타일
리팩터
기타