Skip to content

Commit 255fe44

Browse files
authored
[FRONTEND] DELETE 기능 추가 (#57)
## 📝작업 내용 - 유저 탈퇴 기능 추가 및 로직 개선
1 parent 4fd4b72 commit 255fe44

File tree

14 files changed

+394
-151
lines changed

14 files changed

+394
-151
lines changed

frontend/KAKAO_OAUTH_SETUP.md

Lines changed: 0 additions & 33 deletions
This file was deleted.

frontend/src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import MyPage from "./pages/myPage";
99
import SignIn from "./pages/signIn";
1010
import Setting from "./pages/setting";
1111
import SecondSetting from "./pages/secondSetting";
12-
import RedirectPage from "./pages/oauth/kakao/RedirectPage";
12+
import RedirectPage from "./pages/oauth/kakao/redirectPage";
1313
import ExpertVerifyLayout from "./components/layout/expertVerifyLayout";
1414
import { ProtectedRoute } from "./components/common/ProtectedRoute";
1515

frontend/src/api/userSetting/apiClient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ apiClient.interceptors.response.use(
122122
if (
123123
error.response?.status === 401 &&
124124
!originalRequest._retry &&
125-
!originalRequest.url?.includes('/oauth/refresh') // 토큰 갱신 API는 제외
125+
!originalRequest.url?.includes('/refresh') // 토큰 갱신 API는 제외
126126
) {
127127
originalRequest._retry = true;
128128

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { useMutation } from "@tanstack/react-query";
2+
import { useAuthStore } from "../../stores/useAuthStore";
3+
import { useTokenStore } from "../../stores/useTokenStore";
4+
import { useNavigate } from "react-router-dom";
5+
import { deleteUser } from "./userService";
6+
7+
export const useDeleteUser = () => {
8+
const { clearAuthState } = useAuthStore();
9+
const { clearTokens } = useTokenStore();
10+
const navigate = useNavigate();
11+
12+
const mutation = useMutation({
13+
mutationFn: deleteUser,
14+
onSuccess: (data) => {
15+
console.log('✅ 사용자 삭제 성공:', data);
16+
17+
// 로컬 상태 정리
18+
clearTokens();
19+
clearAuthState();
20+
21+
// 삭제 완료 후 로그인 페이지로 이동
22+
setTimeout(() => {
23+
navigate('/signIn');
24+
}, 100);
25+
},
26+
onError: (error: Error) => {
27+
console.error('❌ 사용자 삭제 실패:', error.message);
28+
29+
// 401 에러 등 인증 오류인 경우 로그아웃 처리
30+
if (error.message.includes('401') || error.message.includes('인증')) {
31+
clearTokens();
32+
clearAuthState();
33+
navigate('/signIn');
34+
}
35+
},
36+
});
37+
38+
const deleteUserAccount = () => {
39+
// 사용자 확인 후 삭제 실행
40+
if (window.confirm('정말로 계정을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.')) {
41+
mutation.mutate();
42+
}
43+
};
44+
45+
return {
46+
deleteUserAccount,
47+
isLoading: mutation.isPending,
48+
error: mutation.error
49+
};
50+
};

frontend/src/api/userSetting/useKakaoLogin.ts

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,9 @@ import axios from "axios";
33
import { useMutation } from "@tanstack/react-query";
44
import { useAuthStore } from "../../stores/useAuthStore";
55
import { useTokenStore } from "../../stores/useTokenStore";
6-
import { useNavigate } from "react-router-dom"; // useNavigate import
7-
86
export const useKakaoLogin = () => {
97
const { setLoading, setAuthError, setAuthenticated } = useAuthStore();
108
const { setTokens } = useTokenStore();
11-
const navigate = useNavigate(); // useNavigate 훅 사용
129

1310
const mutation = useMutation({
1411
mutationFn: async (code: string) => {
@@ -26,20 +23,16 @@ export const useKakaoLogin = () => {
2623
return response.data;
2724
},
2825
onSuccess: (data) => {
29-
console.log('✅ 로그인 성공');
26+
console.log('✅ 로그인 성공', data);
3027

31-
const { access_token, refresh_token, user_info } = data.data;
28+
const { access_token, refresh_token } = data.data;
3229

3330
if (access_token && refresh_token) {
3431
setTokens(access_token, refresh_token);
3532
setAuthenticated(true);
36-
37-
// 백엔드에서 보내준 user_info.initialized 값으로 바로 분기 처리
38-
if (user_info && user_info.initialized) {
39-
navigate("/home");
40-
} else {
41-
navigate("/setting");
42-
}
33+
34+
console.log('🔄 토큰 저장 완료, RedirectPage에서 라우팅 처리 예정');
35+
// 사용자 정보는 별도의 /users/me API 호출로 가져와야 함
4336
}
4437
setLoading(false);
4538
},

frontend/src/api/userSetting/useLogout.ts

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -35,21 +35,8 @@ export const useLogout = () => {
3535
if (response.data.success) {
3636
console.log('✅ 서버 로그아웃 성공:', response.data.message);
3737

38-
// 카카오 세션도 해제 (선택적)
39-
try {
40-
const iframe = document.createElement('iframe');
41-
iframe.style.display = 'none';
42-
iframe.src = `https://kauth.kakao.com/oauth/logout?client_id=${import.meta.env.VITE_KAKAO_CLIENT_ID}&logout_redirect_uri=${encodeURIComponent(window.location.origin)}`;
43-
document.body.appendChild(iframe);
44-
45-
setTimeout(() => {
46-
document.body.removeChild(iframe);
47-
}, 3000);
48-
49-
console.log('🔄 카카오 세션 해제 요청 완료');
50-
} catch (kakaoError) {
51-
console.warn('⚠️ 카카오 세션 해제 실패:', kakaoError);
52-
}
38+
// 카카오 세션은 해제하지 않음 (재로그인 시 기존 사용자로 인식하기 위해)
39+
console.log('ℹ️ 카카오 세션은 유지됩니다. 재로그인 시 기존 계정으로 연결됩니다.');
5340
} else {
5441
console.warn('⚠️ 서버 로그아웃 응답 실패:', response.data);
5542
}

frontend/src/api/userSetting/userService.ts

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,15 @@ export const useUser = (options?: { enabled?: boolean }) => {
1919
})
2020
}
2121

22-
export const updateUserProfile = async (userData: {
22+
export const initializeUserProfile = async (userData: {
2323
role: UserData['role'];
24-
interests?: Array<{ interestId: number; name: string }>;
24+
interest_ids: number[];
25+
custom_interests?: string[];
2526
}) => {
2627
const { accessToken } = useTokenStore.getState();
2728

2829
console.log('API 요청 데이터:', {
29-
url: `${API_URL}/users/me`,
30+
url: `${API_URL}/users/initialize`,
3031
requestData: userData,
3132
hasToken: !!accessToken,
3233
tokenPreview: accessToken ? `${accessToken.substring(0, 10)}...` : 'null'
@@ -37,7 +38,7 @@ export const updateUserProfile = async (userData: {
3738
}
3839

3940
try {
40-
const response = await apiClient.put('/users/me', userData);
41+
const response = await apiClient.put('/users/initialize', userData);
4142

4243
console.log('API 응답:', response.data);
4344
return response.data;
@@ -53,4 +54,36 @@ export const updateUserProfile = async (userData: {
5354
}
5455
throw error;
5556
}
57+
};
58+
59+
export const deleteUser = async () => {
60+
const { accessToken } = useTokenStore.getState();
61+
62+
console.log('사용자 삭제 API 호출:', {
63+
url: `${API_URL}/users`,
64+
hasToken: !!accessToken,
65+
tokenPreview: accessToken ? `${accessToken.substring(0, 10)}...` : 'null'
66+
});
67+
68+
if (!accessToken) {
69+
throw new Error('액세스 토큰이 없습니다.');
70+
}
71+
72+
try {
73+
const response = await apiClient.delete('/users');
74+
75+
console.log('사용자 삭제 API 응답:', response.data);
76+
return response.data;
77+
} catch (error: unknown) {
78+
if (error && typeof error === 'object' && 'response' in error) {
79+
const axiosError = error as { response: { status: number; data: unknown } };
80+
console.error('사용자 삭제 API 에러 상세:', {
81+
status: axiosError.response?.status,
82+
data: axiosError.response?.data
83+
});
84+
} else {
85+
console.error('사용자 삭제 API 에러:', error);
86+
}
87+
throw error;
88+
}
5689
};
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { useState } from 'react';
2+
import { useDeleteUser } from '../../api/userSetting/useDeleteUser';
3+
4+
interface DeleteAccountModalProps {
5+
isOpen: boolean;
6+
onClose: () => void;
7+
}
8+
9+
export default function DeleteAccountModal({ isOpen, onClose }: DeleteAccountModalProps) {
10+
const [confirmText, setConfirmText] = useState('');
11+
const { deleteUserAccount, isLoading } = useDeleteUser();
12+
13+
const CONFIRM_TEXT = '계정삭제';
14+
const isConfirmValid = confirmText === CONFIRM_TEXT;
15+
16+
const handleDelete = () => {
17+
if (isConfirmValid) {
18+
deleteUserAccount();
19+
onClose(); // 모달 닫기
20+
}
21+
};
22+
23+
const handleClose = () => {
24+
setConfirmText('');
25+
onClose();
26+
};
27+
28+
if (!isOpen) return null;
29+
30+
return (
31+
<div style={{
32+
position: 'fixed',
33+
top: 0,
34+
left: 0,
35+
right: 0,
36+
bottom: 0,
37+
backgroundColor: 'rgba(0, 0, 0, 0.5)',
38+
display: 'flex',
39+
alignItems: 'center',
40+
justifyContent: 'center',
41+
zIndex: 1000
42+
}}>
43+
<div style={{
44+
backgroundColor: 'white',
45+
padding: '30px',
46+
borderRadius: '10px',
47+
maxWidth: '400px',
48+
width: '90%',
49+
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.1)'
50+
}}>
51+
<h2 style={{ color: '#dc3545', marginBottom: '20px', textAlign: 'center' }}>
52+
⚠️ 계정 삭제
53+
</h2>
54+
55+
<div style={{ marginBottom: '20px', lineHeight: '1.6' }}>
56+
<p style={{ marginBottom: '15px' }}>
57+
<strong>계정을 삭제하면 다음과 같은 데이터가 영구적으로 삭제됩니다:</strong>
58+
</p>
59+
<ul style={{ paddingLeft: '20px', marginBottom: '15px' }}>
60+
<li>프로필 정보</li>
61+
<li>관심사 설정</li>
62+
<li>포인트 및 활동 기록</li>
63+
<li>모든 개인 데이터</li>
64+
</ul>
65+
<p style={{ color: '#dc3545', fontWeight: 'bold' }}>
66+
이 작업은 되돌릴 수 없습니다.
67+
</p>
68+
</div>
69+
70+
<div style={{ marginBottom: '20px' }}>
71+
<label style={{ display: 'block', marginBottom: '10px', fontWeight: 'bold' }}>
72+
계속하려면 '<span style={{ color: '#dc3545' }}>{CONFIRM_TEXT}</span>'를 입력하세요:
73+
</label>
74+
<input
75+
type="text"
76+
value={confirmText}
77+
onChange={(e) => setConfirmText(e.target.value)}
78+
placeholder={CONFIRM_TEXT}
79+
style={{
80+
width: '100%',
81+
padding: '10px',
82+
border: '2px solid #ddd',
83+
borderRadius: '5px',
84+
fontSize: '16px',
85+
borderColor: isConfirmValid ? '#28a745' : '#ddd'
86+
}}
87+
disabled={isLoading}
88+
/>
89+
</div>
90+
91+
<div style={{ display: 'flex', gap: '10px', justifyContent: 'flex-end' }}>
92+
<button
93+
onClick={handleClose}
94+
disabled={isLoading}
95+
style={{
96+
padding: '10px 20px',
97+
backgroundColor: '#6c757d',
98+
color: 'white',
99+
border: 'none',
100+
borderRadius: '5px',
101+
cursor: isLoading ? 'not-allowed' : 'pointer',
102+
opacity: isLoading ? 0.6 : 1
103+
}}
104+
>
105+
취소
106+
</button>
107+
108+
<button
109+
onClick={handleDelete}
110+
disabled={!isConfirmValid || isLoading}
111+
style={{
112+
padding: '10px 20px',
113+
backgroundColor: isConfirmValid ? '#dc3545' : '#ccc',
114+
color: 'white',
115+
border: 'none',
116+
borderRadius: '5px',
117+
cursor: (!isConfirmValid || isLoading) ? 'not-allowed' : 'pointer',
118+
opacity: (!isConfirmValid || isLoading) ? 0.6 : 1
119+
}}
120+
>
121+
{isLoading ? '삭제 중...' : '계정 삭제'}
122+
</button>
123+
</div>
124+
</div>
125+
</div>
126+
);
127+
}

0 commit comments

Comments
 (0)