Skip to content

Commit

Permalink
Merge pull request #86 from KDT-Web-IDE-Project/79-feat
Browse files Browse the repository at this point in the history
feat: 마이페이지 api 연결
  • Loading branch information
yundol777 authored Jan 31, 2025
2 parents c81530e + af4a5d1 commit a962e6c
Show file tree
Hide file tree
Showing 7 changed files with 202 additions and 33 deletions.
55 changes: 50 additions & 5 deletions src/apis/axiosInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,73 @@ const axiosInstance = axios.create({
withCredentials: true, // 쿠키 설정
});

// 요청 인터셉터
// 로그인 API를 다시 호출하여 새로운 토큰 발급
const refreshAccessToken = async () => {
try {
const refreshToken = localStorage.getItem('refreshToken');
const loginId = localStorage.getItem('loginId'); // 로그인 시 저장해둔 ID

if (!refreshToken || !loginId) {
throw new Error('Refresh Token 또는 로그인 ID 없음');
}

// 로그인 API 재호출
const response = await axios.post(
`${import.meta.env.VITE_BASE_URL}/api/auth/login`,
{
loginId, // 기존 로그인 ID를 함께 전달
}
);

// 새롭게 발급받은 토큰을 저장
const { accessToken, refreshToken: newRefreshToken } =
response.data.tokenResponse;
localStorage.setItem('token', accessToken);
localStorage.setItem('refreshToken', newRefreshToken);

return accessToken;
} catch (error) {
console.error('토큰 갱신 실패:', error);
localStorage.clear();
location.href = '/sign-in';
return null;
}
};

// 요청 인터셉터 - 모든 요청에 Access Token 추가
axiosInstance.interceptors.request.use(
async (config) => {
// 토큰 추가
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
} else {
localStorage.clear();
localStorage.removeItem('token');
localStorage.removeItem('refreshToken');
localStorage.removeItem('loginId');
location.href = '/sign-in';
}
return config;
},
(error) => Promise.reject(error)
);

// 응답 인터셉터
// 응답 인터셉터 - Access Token 만료 시 자동 갱신
axiosInstance.interceptors.response.use(
async (response) => response,
async (response) => response.data,

async (error) => {
// 에러 처리 로직 추가
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true; // 무한 루프 방지

const newAccessToken = await refreshAccessToken();
if (newAccessToken) {
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
return axiosInstance(originalRequest); // 요청 다시 보내기
}
}

console.error('API Error:', error);
return Promise.reject(error);
}
Expand Down
14 changes: 9 additions & 5 deletions src/apis/queryClient.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
// src/queryClient.ts
import { QueryClient } from '@tanstack/react-query';

export const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 1, // 실패 시 재시도 횟수
refetchOnWindowFocus: false, // 창이 포커스될 때 데이터 리패치 방지
staleTime: 5 * 60 * 1000, // 데이터가 신선한 상태로 유지되는 시간 (5분)
retry: 3, // 실패 시 최대 3번 재시도 (네트워크 불안정성 보완)
refetchOnWindowFocus: true, // 창 포커스 시 리패치 활성화 (실시간성 강화)
refetchOnReconnect: true, // 네트워크 재연결 시 데이터 리패치
staleTime: 0, // 데이터는 항상 신선하지 않다고 간주 (실시간 데이터 반영)
gcTime: 5 * 60 * 1000, // 캐시는 5분 동안 유지 (짧은 시간 안에 동일 데이터를 캐시 활용)
},
mutations: {
retry: 1, // 실패 시 재시도 횟수
retry: 1, // Mutation은 1번만 재시도 (중복 작업 방지)
onError: (error) => {
console.error('Mutation 에러:', error);
},
},
},
});
Empty file removed src/hooks/.gitkeep
Empty file.
30 changes: 30 additions & 0 deletions src/hooks/Auth/useUpdateProfile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { http } from '../../apis/httpClient';

export const useUpdateProfile = () => ({
patchUserProfileImage: async (image: File) => {
const formData = new FormData();
formData.append('image', image);

const data = await http.patch(`/api/auth/profile/profile-image`, formData, {
headers: {
'Content-Type': 'multipart/form-data', // 헤더에 multipart/form-data 설정
},
});

return data;
},

patchUserNickName: async (nickName: string) => {
const data = await http.patch(`/api/auth/profile/nickname`, null, {
params: { nickName },
});
return data;
},

patchUserLoginId: async (loginId: string) => {
const data = await http.patch('/api/auth/profile/login-id', null, {
params: { loginId },
});
return data;
},
});
14 changes: 14 additions & 0 deletions src/hooks/Auth/useUserProfile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useQuery } from '@tanstack/react-query';
import { UserProfileResponse } from '../../models/Auth';
import { http } from '../../apis/httpClient';

export const useUserProfile = () => {
return useQuery<UserProfileResponse>({
queryKey: ['userInfo'],
queryFn: async () => {
const data = await http.get<UserProfileResponse>(`/api/auth/profile`);
return data;
},
staleTime: 1000,
});
};
7 changes: 7 additions & 0 deletions src/models/Auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,10 @@ export interface AuthButtonProps {
disabled: boolean;
text: string;
}

export interface UserProfileResponse {
loginId: string;
memberId: number;
nickName: string;
profileImage: string;
}
115 changes: 92 additions & 23 deletions src/pages/MyPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,79 @@ import {
UncheckedCircle,
UserInfoSection,
} from './style';
import { useUserProfile } from '../../hooks/Auth/useUserProfile';
import { useUpdateProfile } from '../../hooks/Auth/useUpdateProfile';

const MyPage: React.FC = () => {
const userName = '유지희';
const userId = 'esder1310';
const { data, isLoading, error, refetch } = useUserProfile();
const { patchUserNickName, patchUserLoginId, patchUserProfileImage } =
useUpdateProfile();

const profileImageUploadRef = React.useRef<HTMLInputElement>(null);

const [isEditingId, setIsEditingId] = useState(false);
const [isEditingName, setIsEditingName] = useState(false);
const [id, setId] = useState(data?.loginId ?? '');

const [isEditingNickName, setIsEditingNickName] = useState(false);
const [nickName, setNickName] = useState(data?.nickName ?? '');

const [selectedTheme, setSelectedTheme] = useState('dark');

const handleThemeChange = (theme: string) => {
setSelectedTheme(theme);
};

const handleProfileImageUploadButtonClick = () => {
profileImageUploadRef.current?.click();
};

const handleProfileImageChange = async (
event: React.ChangeEvent<HTMLInputElement>
) => {
if (event.target.files && event.target.files[0]) {
try {
const file = event.target.files[0];
await patchUserProfileImage(file);
} catch (error) {
console.error('프로필 이미지 수정 실패:', error);
}
}
};

const handleNickNameChange = async () => {
try {
await patchUserNickName(nickName);
setIsEditingNickName(false);
refetch();
} catch (error) {
console.error('닉네임 수정 실패', error);
}
};

const handleIdChange = async () => {
try {
await patchUserLoginId(id);
setIsEditingId(false);

localStorage.removeItem('token');
localStorage.removeItem('refreshToken');
localStorage.removeItem('loginId');

alert('아이디가 변경되었습니다. 다시 로그인해주세요.');
location.href = '/sign-in';
} catch (error) {
console.error('아이디 수정 실패', error);
}
};

if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error...</div>;

return (
<MyPageContainer>
<MyPageHeader>
<MyPageHeaderUserName>{userName}</MyPageHeaderUserName>님의 마이페이지
<MyPageHeaderUserName>{data?.nickName}</MyPageHeaderUserName>님의
마이페이지
</MyPageHeader>

<MyPageContent>
Expand All @@ -48,8 +104,19 @@ const MyPage: React.FC = () => {
</h2>

<ProfileImage>
<img src={defaultImg} alt="프로필 이미지" />
<button aria-label="프로필 이미지 수정">
<img src={data?.profileImage || defaultImg} alt="프로필 이미지" />
<button
aria-label="프로필 이미지 수정"
onClick={handleProfileImageUploadButtonClick}
>
<input
type="file"
accept="image/*"
id="profileImageUpload"
onChange={handleProfileImageChange}
ref={profileImageUploadRef}
style={{ display: 'none' }}
/>
<BsImageFill className="icon_image" />
</button>
</ProfileImage>
Expand All @@ -70,45 +137,47 @@ const MyPage: React.FC = () => {
<Input
type="id"
id="id"
value={userId}
onChange={() => {}}
value={id}
onChange={(e) => {
setId(e.target.value);
}}
placeholder="수정할 아이디를 입력하세요."
/>
<button onClick={() => setIsEditingId(false)}>
수정완료
</button>
<button onClick={handleIdChange}>수정완료</button>
</EditInfo>
) : (
<ProfileInfoDetailsContent>{userId}</ProfileInfoDetailsContent>
<ProfileInfoDetailsContent>
{data?.loginId}
</ProfileInfoDetailsContent>
)}
</ProfileInfoDetails>

<ProfileInfoDetails>
<label htmlFor="name">
| 사용자 이름
| 사용자 닉네임
<button
onClick={() => setIsEditingName(true)}
aria-label="사용자 이름 수정"
onClick={() => setIsEditingNickName(true)}
aria-label="사용자 닉네임 수정"
>
<BsPencilFill className="icon_edit" />
</button>
</label>
{isEditingName ? (
{isEditingNickName ? (
<EditInfo>
<Input
type="text"
id="name"
value={userName}
onChange={() => {}}
placeholder="수정할 이름 입력하세요."
value={nickName}
onChange={(e) => {
setNickName(e.target.value);
}}
placeholder="수정할 닉네임을 입력하세요."
/>
<button onClick={() => setIsEditingName(false)}>
수정완료
</button>
<button onClick={handleNickNameChange}>수정완료</button>
</EditInfo>
) : (
<ProfileInfoDetailsContent>
{userName}
{data?.nickName}
</ProfileInfoDetailsContent>
)}
</ProfileInfoDetails>
Expand Down

0 comments on commit a962e6c

Please sign in to comment.