mirror of
https://github.com/ktds-dg0501/kt-event-marketing-fe.git
synced 2026-06-13 02:19:12 +00:00
User API 전체 연동 완료 및 로그아웃 에러 처리 개선
## 주요 변경사항 ### 1. FSD 아키텍처 기반 API 레이어 구축 - entities/user: User 엔티티 (타입, API) - features/auth: 인증 기능 (useAuth, AuthProvider) - shared/api: 공통 API 클라이언트 (Axios, 인터셉터) ### 2. 전체 User API 화면 연동 완료 - ✅ POST /api/v1/users/login → login/page.tsx - ✅ POST /api/v1/users/register → register/page.tsx - ✅ POST /api/v1/users/logout → profile/page.tsx - ✅ GET /api/v1/users/profile → profile/page.tsx - ✅ PUT /api/v1/users/profile → profile/page.tsx - ✅ PUT /api/v1/users/password → profile/page.tsx ### 3. 로그인 페이지 API 연동 - useAuthStore → useAuthContext 변경 - 실제 로그인 API 호출 - 비밀번호 검증 완화 (API 스펙에 맞춤) - 상세 로깅 추가 ### 4. 프로필 페이지 API 연동 - 프로필 자동 로드 (GET /profile) - 프로필 수정 (PUT /profile) - 비밀번호 변경 (PUT /password) - 로그아웃 (POST /logout) - 전화번호 형식 변환 (01012345678 ↔ 010-1234-5678) ### 5. 로그아웃 에러 처리 개선 - 백엔드 500 에러 발생해도 로컬 상태 정리 후 로그아웃 진행 - 사용자 경험 우선: 정상 로그아웃으로 처리 - 개발자용 상세 에러 로그 출력 ### 6. 문서화 - docs/api-integration-complete.md: 전체 연동 완료 보고서 - docs/api-server-issue.md: 백엔드 이슈 상세 보고 (회원가입 타임아웃, 로그아웃 500 에러) - docs/user-api-integration.md: User API 통합 가이드 - docs/register-api-guide.md: 회원가입 API 가이드 ### 7. 에러 처리 강화 - 서버 응답 에러 / 네트워크 에러 / 요청 설정 에러 구분 - 사용자 친화적 에러 메시지 - 전체 프로세스 상세 로깅 ## 기술 스택 - FSD Architecture - React Context API (AuthProvider) - Axios (인터셉터, 90초 타임아웃) - Zod (폼 검증) - TypeScript (엄격한 타입) ## 테스트 - ✅ 빌드 성공 - ⏳ 백엔드 안정화 후 전체 플로우 테스트 필요 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -19,7 +19,7 @@ import {
|
||||
IconButton,
|
||||
} from '@mui/material';
|
||||
import { Visibility, VisibilityOff, Email, Lock, ChatBubble } from '@mui/icons-material';
|
||||
import { useAuthStore } from '@/stores/authStore';
|
||||
import { useAuthContext } from '@/features/auth';
|
||||
import { useUIStore } from '@/stores/uiStore';
|
||||
import { getGradientButtonStyle, responsiveText } from '@/shared/lib/button-styles';
|
||||
|
||||
@@ -31,8 +31,7 @@ const loginSchema = z.object({
|
||||
.email('올바른 이메일 형식이 아닙니다'),
|
||||
password: z
|
||||
.string()
|
||||
.min(8, '비밀번호는 8자 이상이어야 합니다')
|
||||
.regex(/^(?=.*[A-Za-z])(?=.*\d)/, '영문과 숫자를 포함해야 합니다'),
|
||||
.min(1, '비밀번호를 입력해주세요'),
|
||||
rememberMe: z.boolean().optional(),
|
||||
});
|
||||
|
||||
@@ -40,7 +39,7 @@ type LoginFormData = z.infer<typeof loginSchema>;
|
||||
|
||||
export default function LoginPage() {
|
||||
const router = useRouter();
|
||||
const { login } = useAuthStore();
|
||||
const { login } = useAuthContext();
|
||||
const { showToast, setLoading } = useUIStore();
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
@@ -58,32 +57,28 @@ export default function LoginPage() {
|
||||
});
|
||||
|
||||
const onSubmit = async (data: LoginFormData) => {
|
||||
console.log('🔐 로그인 시도:', { email: data.email });
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// TODO: API 연동 시 실제 로그인 처리
|
||||
// const response = await axios.post(`${USER_HOST}/api/v1/auth/login`, {
|
||||
// email: data.email,
|
||||
// password: data.password,
|
||||
// });
|
||||
|
||||
// 임시 로그인 처리 (API 연동 전)
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
const mockUser = {
|
||||
id: '1',
|
||||
name: '홍길동',
|
||||
phone: '010-1234-5678',
|
||||
// User API 호출
|
||||
const result = await login({
|
||||
email: data.email,
|
||||
businessName: '홍길동 고깃집',
|
||||
businessType: 'restaurant',
|
||||
};
|
||||
password: data.password,
|
||||
});
|
||||
|
||||
login(mockUser, 'mock-jwt-token');
|
||||
showToast('로그인되었습니다', 'success');
|
||||
router.push('/');
|
||||
} catch {
|
||||
showToast('로그인에 실패했습니다. 이메일과 비밀번호를 확인해주세요.', 'error');
|
||||
if (result.success) {
|
||||
console.log('✅ 로그인 성공:', result.user);
|
||||
showToast('로그인되었습니다', 'success');
|
||||
router.push('/');
|
||||
} else {
|
||||
console.error('❌ 로그인 실패:', result.error);
|
||||
showToast(result.error || '로그인에 실패했습니다. 이메일과 비밀번호를 확인해주세요.', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('💥 로그인 예외:', error);
|
||||
showToast('로그인 중 오류가 발생했습니다. 다시 시도해주세요.', 'error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -26,18 +26,18 @@ import {
|
||||
import { ArrowBack, Visibility, VisibilityOff, CheckCircle } from '@mui/icons-material';
|
||||
import { useState, useEffect, Suspense } from 'react';
|
||||
import { useUIStore } from '@/stores/uiStore';
|
||||
import { useAuthStore } from '@/stores/authStore';
|
||||
import { useAuthContext } from '@/features/auth';
|
||||
import { getGradientButtonStyle, responsiveText } from '@/shared/lib/button-styles';
|
||||
|
||||
// 각 단계별 유효성 검사 스키마
|
||||
const step1Schema = z
|
||||
.object({
|
||||
email: z.string().email('올바른 이메일 형식이 아닙니다'),
|
||||
email: z.string().min(1, '이메일을 입력해주세요').email('올바른 이메일 형식이 아닙니다'),
|
||||
password: z
|
||||
.string()
|
||||
.min(8, '비밀번호는 8자 이상이어야 합니다')
|
||||
.regex(/^(?=.*[A-Za-z])(?=.*\d)/, '영문과 숫자를 포함해야 합니다'),
|
||||
confirmPassword: z.string(),
|
||||
.max(100, '비밀번호는 100자 이하여야 합니다'),
|
||||
confirmPassword: z.string().min(1, '비밀번호 확인을 입력해주세요'),
|
||||
})
|
||||
.refine((data) => data.password === data.confirmPassword, {
|
||||
message: '비밀번호가 일치하지 않습니다',
|
||||
@@ -45,7 +45,7 @@ const step1Schema = z
|
||||
});
|
||||
|
||||
const step2Schema = z.object({
|
||||
name: z.string().min(2, '이름은 2자 이상이어야 합니다'),
|
||||
name: z.string().min(2, '이름은 2자 이상이어야 합니다').max(50, '이름은 50자 이하여야 합니다'),
|
||||
phone: z
|
||||
.string()
|
||||
.min(1, '휴대폰 번호를 입력해주세요')
|
||||
@@ -53,13 +53,14 @@ const step2Schema = z.object({
|
||||
});
|
||||
|
||||
const step3Schema = z.object({
|
||||
businessName: z.string().min(2, '상호명은 2자 이상이어야 합니다'),
|
||||
businessName: z.string().min(2, '상호명은 2자 이상이어야 합니다').max(100, '상호명은 100자 이하여야 합니다'),
|
||||
businessNumber: z
|
||||
.string()
|
||||
.min(1, '사업자 번호를 입력해주세요')
|
||||
.regex(/^\d{3}-\d{2}-\d{5}$/, '올바른 사업자 번호 형식이 아닙니다 (123-45-67890)'),
|
||||
businessType: z.string().min(1, '업종을 선택해주세요'),
|
||||
businessLocation: z.string().optional(),
|
||||
businessType: z.string().min(1, '업종을 선택해주세요').max(50, '업종은 50자 이하여야 합니다'),
|
||||
businessLocation: z.string().max(255, '주소는 255자 이하여야 합니다').optional(),
|
||||
businessHours: z.string().max(255, '영업시간은 255자 이하여야 합니다').optional(),
|
||||
agreeTerms: z.boolean().refine((val) => val === true, {
|
||||
message: '이용약관에 동의해주세요',
|
||||
}),
|
||||
@@ -79,7 +80,7 @@ function RegisterForm() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { showToast, setLoading } = useUIStore();
|
||||
const { login } = useAuthStore();
|
||||
const { register: registerUser } = useAuthContext();
|
||||
|
||||
// URL 쿼리에서 step 파라미터 읽기 (기본값: 1)
|
||||
const stepParam = searchParams.get('step');
|
||||
@@ -206,33 +207,62 @@ function RegisterForm() {
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
console.log('📝 Step 3 검증 시작');
|
||||
|
||||
if (!validateStep(3)) {
|
||||
console.error('❌ Step 3 검증 실패');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('✅ Step 3 검증 통과');
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
console.log('🔄 회원가입 프로세스 시작');
|
||||
|
||||
// TODO: API 연동 시 실제 회원가입 처리
|
||||
// const response = await axios.post(`${USER_HOST}/api/v1/auth/register`, formData);
|
||||
// 전화번호 형식 변환: 010-1234-5678 -> 01012345678
|
||||
const phoneNumber = formData.phone!.replace(/-/g, '');
|
||||
console.log('📞 전화번호 변환:', formData.phone, '->', phoneNumber);
|
||||
|
||||
// 임시 처리
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
const mockUser = {
|
||||
id: '1',
|
||||
// API 요청 데이터 구성
|
||||
const registerData = {
|
||||
name: formData.name!,
|
||||
phone: formData.phone!,
|
||||
phoneNumber: phoneNumber,
|
||||
email: formData.email!,
|
||||
businessName: formData.businessName!,
|
||||
businessType: formData.businessType!,
|
||||
password: formData.password!,
|
||||
storeName: formData.businessName!,
|
||||
industry: formData.businessType || '',
|
||||
address: formData.businessLocation || '',
|
||||
businessHours: formData.businessHours || '',
|
||||
};
|
||||
|
||||
login(mockUser, 'mock-jwt-token');
|
||||
setSuccessDialogOpen(true);
|
||||
} catch {
|
||||
showToast('회원가입에 실패했습니다. 다시 시도해주세요.', 'error');
|
||||
console.log('📦 회원가입 요청 데이터:', {
|
||||
...registerData,
|
||||
password: '***' // 비밀번호는 로그에 표시 안 함
|
||||
});
|
||||
|
||||
// User API 호출
|
||||
console.log('🚀 registerUser 함수 호출');
|
||||
const result = await registerUser(registerData);
|
||||
console.log('📥 registerUser 결과:', result);
|
||||
|
||||
if (result.success) {
|
||||
console.log('✅ 회원가입 성공:', result.user);
|
||||
showToast('회원가입이 완료되었습니다!', 'success');
|
||||
setSuccessDialogOpen(true);
|
||||
} else {
|
||||
console.error('❌ 회원가입 실패:', result.error);
|
||||
showToast(result.error || '회원가입에 실패했습니다. 다시 시도해주세요.', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('💥 회원가입 예외 발생:', error);
|
||||
if (error instanceof Error) {
|
||||
console.error('오류 메시지:', error.message);
|
||||
console.error('오류 스택:', error.stack);
|
||||
}
|
||||
showToast('회원가입 중 오류가 발생했습니다. 다시 시도해주세요.', 'error');
|
||||
} finally {
|
||||
console.log('🏁 회원가입 프로세스 종료');
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
@@ -314,11 +344,11 @@ function RegisterForm() {
|
||||
fullWidth
|
||||
label="비밀번호"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
placeholder="8자 이상, 영문+숫자 조합"
|
||||
placeholder="8자 이상"
|
||||
value={formData.password || ''}
|
||||
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
||||
error={!!errors.password}
|
||||
helperText={errors.password}
|
||||
helperText={errors.password || '비밀번호는 8자 이상이어야 합니다'}
|
||||
required
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
@@ -548,10 +578,19 @@ function RegisterForm() {
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="주요 지역"
|
||||
placeholder="예: 강남구"
|
||||
label="주소"
|
||||
placeholder="예: 서울특별시 강남구 테헤란로 123"
|
||||
value={formData.businessLocation || ''}
|
||||
onChange={(e) => setFormData({ ...formData, businessLocation: e.target.value })}
|
||||
helperText="사업장 주소를 입력해주세요"
|
||||
/>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="영업시간"
|
||||
placeholder="예: 평일 09:00-18:00, 주말 휴무"
|
||||
value={formData.businessHours || ''}
|
||||
onChange={(e) => setFormData({ ...formData, businessHours: e.target.value })}
|
||||
helperText="선택 사항입니다"
|
||||
/>
|
||||
|
||||
|
||||
+150
-34
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
@@ -26,8 +26,9 @@ import {
|
||||
DialogActions,
|
||||
} from '@mui/material';
|
||||
import { Person, Visibility, VisibilityOff, CheckCircle } from '@mui/icons-material';
|
||||
import { useAuthStore } from '@/stores/authStore';
|
||||
import { useAuthContext } from '@/features/auth';
|
||||
import { useUIStore } from '@/stores/uiStore';
|
||||
import { userApi } from '@/entities/user';
|
||||
import Header from '@/shared/ui/Header';
|
||||
import { cardStyles, colors, responsiveText } from '@/shared/lib/button-styles';
|
||||
|
||||
@@ -56,7 +57,7 @@ const passwordSchema = z
|
||||
newPassword: z
|
||||
.string()
|
||||
.min(8, '비밀번호는 8자 이상이어야 합니다')
|
||||
.regex(/^(?=.*[A-Za-z])(?=.*\d)/, '영문과 숫자를 포함해야 합니다'),
|
||||
.max(100, '비밀번호는 100자 이하여야 합니다'),
|
||||
confirmPassword: z.string(),
|
||||
})
|
||||
.refine((data) => data.newPassword === data.confirmPassword, {
|
||||
@@ -70,25 +71,27 @@ type PasswordData = z.infer<typeof passwordSchema>;
|
||||
|
||||
export default function ProfilePage() {
|
||||
const router = useRouter();
|
||||
const { user, logout, setUser } = useAuthStore();
|
||||
const { user, logout, refreshProfile } = useAuthContext();
|
||||
const { showToast, setLoading } = useUIStore();
|
||||
const [showCurrentPassword, setShowCurrentPassword] = useState(false);
|
||||
const [showNewPassword, setShowNewPassword] = useState(false);
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||
const [successDialogOpen, setSuccessDialogOpen] = useState(false);
|
||||
const [logoutDialogOpen, setLogoutDialogOpen] = useState(false);
|
||||
const [profileLoaded, setProfileLoaded] = useState(false);
|
||||
|
||||
// 기본 정보 폼
|
||||
const {
|
||||
control: basicControl,
|
||||
handleSubmit: handleBasicSubmit,
|
||||
formState: { errors: basicErrors },
|
||||
reset: resetBasic,
|
||||
} = useForm<BasicInfoData>({
|
||||
resolver: zodResolver(basicInfoSchema),
|
||||
defaultValues: {
|
||||
name: user?.name || '',
|
||||
phone: user?.phone || '',
|
||||
email: user?.email || '',
|
||||
name: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -97,11 +100,12 @@ export default function ProfilePage() {
|
||||
control: businessControl,
|
||||
handleSubmit: handleBusinessSubmit,
|
||||
formState: { errors: businessErrors },
|
||||
reset: resetBusiness,
|
||||
} = useForm<BusinessInfoData>({
|
||||
resolver: zodResolver(businessInfoSchema),
|
||||
defaultValues: {
|
||||
businessName: user?.businessName || '',
|
||||
businessType: user?.businessType || '',
|
||||
businessName: '',
|
||||
businessType: '',
|
||||
businessLocation: '',
|
||||
businessHours: '',
|
||||
},
|
||||
@@ -122,6 +126,68 @@ export default function ProfilePage() {
|
||||
},
|
||||
});
|
||||
|
||||
// 프로필 데이터 로드
|
||||
useEffect(() => {
|
||||
const loadProfile = async () => {
|
||||
console.log('📋 프로필 페이지: 프로필 데이터 로드 시작');
|
||||
|
||||
if (!user) {
|
||||
console.log('❌ 사용자 정보 없음, 로그인 페이지로 이동');
|
||||
router.push('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
if (profileLoaded) {
|
||||
console.log('✅ 프로필 이미 로드됨');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
console.log('📡 프로필 조회 API 호출');
|
||||
|
||||
const profile = await userApi.getProfile();
|
||||
console.log('📥 프로필 조회 성공:', profile);
|
||||
|
||||
// 전화번호 형식 변환: 01012345678 → 010-1234-5678
|
||||
const formattedPhone = profile.phoneNumber
|
||||
? `${profile.phoneNumber.slice(0, 3)}-${profile.phoneNumber.slice(3, 7)}-${profile.phoneNumber.slice(7, 11)}`
|
||||
: '';
|
||||
|
||||
// 기본 정보 폼 초기화
|
||||
resetBasic({
|
||||
name: profile.userName || '',
|
||||
phone: formattedPhone,
|
||||
email: profile.email || '',
|
||||
});
|
||||
|
||||
// 사업장 정보 폼 초기화
|
||||
resetBusiness({
|
||||
businessName: profile.storeName || '',
|
||||
businessType: profile.industry || '',
|
||||
businessLocation: profile.address || '',
|
||||
businessHours: profile.businessHours || '',
|
||||
});
|
||||
|
||||
setProfileLoaded(true);
|
||||
console.log('✅ 프로필 폼 초기화 완료');
|
||||
} catch (error: any) {
|
||||
console.error('❌ 프로필 로드 실패:', error);
|
||||
|
||||
if (error.response?.status === 401) {
|
||||
showToast('로그인이 필요합니다', 'error');
|
||||
router.push('/login');
|
||||
} else {
|
||||
showToast('프로필 정보를 불러오는데 실패했습니다', 'error');
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadProfile();
|
||||
}, [user, profileLoaded, router, resetBasic, resetBusiness, setLoading, showToast]);
|
||||
|
||||
const formatPhoneNumber = (value: string) => {
|
||||
const numbers = value.replace(/[^\d]/g, '');
|
||||
if (numbers.length <= 3) return numbers;
|
||||
@@ -130,46 +196,84 @@ export default function ProfilePage() {
|
||||
};
|
||||
|
||||
const onSaveProfile = async (data: BasicInfoData & BusinessInfoData) => {
|
||||
console.log('💾 프로필 저장 시작');
|
||||
console.log('📦 저장 데이터:', { ...data, phone: data.phone });
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// TODO: API 연동 시 실제 프로필 업데이트
|
||||
// await axios.put(`${USER_HOST}/api/v1/users/profile`, data);
|
||||
// 전화번호 형식 변환: 010-1234-5678 → 01012345678
|
||||
const phoneNumber = data.phone.replace(/-/g, '');
|
||||
console.log('📞 전화번호 변환:', data.phone, '->', phoneNumber);
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
const updateData = {
|
||||
userName: data.name,
|
||||
phoneNumber: phoneNumber,
|
||||
storeName: data.businessName,
|
||||
industry: data.businessType,
|
||||
address: data.businessLocation || '',
|
||||
businessHours: data.businessHours || '',
|
||||
};
|
||||
|
||||
if (user) {
|
||||
setUser({
|
||||
...user,
|
||||
...data,
|
||||
});
|
||||
}
|
||||
console.log('📡 프로필 업데이트 API 호출:', updateData);
|
||||
await userApi.updateProfile(updateData);
|
||||
console.log('✅ 프로필 업데이트 성공');
|
||||
|
||||
// 최신 프로필 정보 다시 가져오기
|
||||
console.log('🔄 프로필 새로고침');
|
||||
await refreshProfile();
|
||||
console.log('✅ 프로필 새로고침 완료');
|
||||
|
||||
setSuccessDialogOpen(true);
|
||||
} catch {
|
||||
showToast('프로필 저장에 실패했습니다', 'error');
|
||||
showToast('프로필이 저장되었습니다', 'success');
|
||||
} catch (error: any) {
|
||||
console.error('❌ 프로필 저장 실패:', error);
|
||||
|
||||
let errorMessage = '프로필 저장에 실패했습니다';
|
||||
if (error.response) {
|
||||
errorMessage = error.response.data?.message ||
|
||||
error.response.data?.error ||
|
||||
`서버 오류 (${error.response.status})`;
|
||||
} else if (error.request) {
|
||||
errorMessage = '서버로부터 응답이 없습니다';
|
||||
}
|
||||
|
||||
showToast(errorMessage, 'error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onChangePassword = async (data: PasswordData) => {
|
||||
console.log('Password change data:', data);
|
||||
console.log('🔐 비밀번호 변경 시작');
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// TODO: API 연동 시 실제 비밀번호 변경
|
||||
// await axios.put(`${USER_HOST}/api/v1/users/password`, {
|
||||
// currentPassword: _data.currentPassword,
|
||||
// newPassword: _data.newPassword,
|
||||
// });
|
||||
const passwordData = {
|
||||
currentPassword: data.currentPassword,
|
||||
newPassword: data.newPassword,
|
||||
};
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
console.log('📡 비밀번호 변경 API 호출');
|
||||
await userApi.changePassword(passwordData);
|
||||
console.log('✅ 비밀번호 변경 성공');
|
||||
|
||||
showToast('비밀번호가 변경되었습니다', 'success');
|
||||
resetPassword();
|
||||
} catch {
|
||||
showToast('비밀번호 변경에 실패했습니다', 'error');
|
||||
} catch (error: any) {
|
||||
console.error('❌ 비밀번호 변경 실패:', error);
|
||||
|
||||
let errorMessage = '비밀번호 변경에 실패했습니다';
|
||||
if (error.response) {
|
||||
errorMessage = error.response.data?.message ||
|
||||
error.response.data?.error ||
|
||||
`서버 오류 (${error.response.status})`;
|
||||
} else if (error.request) {
|
||||
errorMessage = '서버로부터 응답이 없습니다';
|
||||
}
|
||||
|
||||
showToast(errorMessage, 'error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -183,9 +287,21 @@ export default function ProfilePage() {
|
||||
})();
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
router.push('/login');
|
||||
const handleLogout = async () => {
|
||||
console.log('🚪 로그아웃 시작');
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
await logout();
|
||||
showToast('로그아웃되었습니다', 'success');
|
||||
} catch (error) {
|
||||
console.error('❌ 로그아웃 중 예상치 못한 에러:', error);
|
||||
showToast('로그아웃되었습니다', 'success');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
// 로그아웃은 항상 로그인 페이지로 이동
|
||||
router.push('/login');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -216,7 +332,7 @@ export default function ProfilePage() {
|
||||
<Person sx={{ fontSize: 56 }} />
|
||||
</Avatar>
|
||||
<Typography variant="h5" sx={{ ...responsiveText.h3, mb: 1 }}>
|
||||
{user?.name}
|
||||
{user?.userName}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ ...responsiveText.body1 }}>
|
||||
{user?.email}
|
||||
@@ -400,7 +516,7 @@ export default function ProfilePage() {
|
||||
label="새 비밀번호"
|
||||
placeholder="새 비밀번호를 입력하세요"
|
||||
error={!!passwordErrors.newPassword}
|
||||
helperText={passwordErrors.newPassword?.message || '8자 이상, 영문과 숫자를 포함해주세요'}
|
||||
helperText={passwordErrors.newPassword?.message || '8자 이상 입력해주세요'}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
|
||||
+4
-1
@@ -1,6 +1,7 @@
|
||||
import type { Metadata, Viewport } from 'next';
|
||||
import { MUIThemeProvider } from '@/shared/lib/theme-provider';
|
||||
import { ReactQueryProvider } from '@/shared/lib/react-query-provider';
|
||||
import { AuthProvider } from '@/features/auth';
|
||||
import '@/styles/globals.css';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -35,7 +36,9 @@ export default function RootLayout({
|
||||
<body>
|
||||
<MUIThemeProvider>
|
||||
<ReactQueryProvider>
|
||||
{children}
|
||||
<AuthProvider>
|
||||
{children}
|
||||
</AuthProvider>
|
||||
</ReactQueryProvider>
|
||||
</MUIThemeProvider>
|
||||
</body>
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import { apiClient } from '@/shared/api';
|
||||
import type {
|
||||
LoginRequest,
|
||||
LoginResponse,
|
||||
RegisterRequest,
|
||||
RegisterResponse,
|
||||
LogoutResponse,
|
||||
ProfileResponse,
|
||||
UpdateProfileRequest,
|
||||
ChangePasswordRequest,
|
||||
} from '../model/types';
|
||||
|
||||
const USER_API_BASE = '/api/v1/users';
|
||||
|
||||
/**
|
||||
* User API Service
|
||||
* 사용자 인증 및 프로필 관리 API
|
||||
*/
|
||||
export const userApi = {
|
||||
/**
|
||||
* 로그인
|
||||
*/
|
||||
login: async (data: LoginRequest): Promise<LoginResponse> => {
|
||||
const response = await apiClient.post<LoginResponse>(
|
||||
`${USER_API_BASE}/login`,
|
||||
data
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 회원가입
|
||||
*/
|
||||
register: async (data: RegisterRequest): Promise<RegisterResponse> => {
|
||||
console.log('📞 userApi.register 호출');
|
||||
console.log('🎯 URL:', `${USER_API_BASE}/register`);
|
||||
console.log('📦 요청 데이터:', {
|
||||
...data,
|
||||
password: '***'
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await apiClient.post<RegisterResponse>(
|
||||
`${USER_API_BASE}/register`,
|
||||
data
|
||||
);
|
||||
console.log('✅ userApi.register 성공:', response.data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('❌ userApi.register 실패:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 로그아웃
|
||||
*/
|
||||
logout: async (): Promise<LogoutResponse> => {
|
||||
const token = localStorage.getItem('accessToken');
|
||||
const response = await apiClient.post<LogoutResponse>(
|
||||
`${USER_API_BASE}/logout`,
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 프로필 조회
|
||||
*/
|
||||
getProfile: async (): Promise<ProfileResponse> => {
|
||||
const response = await apiClient.get<ProfileResponse>(
|
||||
`${USER_API_BASE}/profile`
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 프로필 수정
|
||||
*/
|
||||
updateProfile: async (
|
||||
data: UpdateProfileRequest
|
||||
): Promise<ProfileResponse> => {
|
||||
const response = await apiClient.put<ProfileResponse>(
|
||||
`${USER_API_BASE}/profile`,
|
||||
data
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 비밀번호 변경
|
||||
*/
|
||||
changePassword: async (data: ChangePasswordRequest): Promise<void> => {
|
||||
await apiClient.put(`${USER_API_BASE}/password`, data);
|
||||
},
|
||||
};
|
||||
|
||||
export default userApi;
|
||||
@@ -0,0 +1,16 @@
|
||||
// Types
|
||||
export type {
|
||||
LoginRequest,
|
||||
LoginResponse,
|
||||
RegisterRequest,
|
||||
RegisterResponse,
|
||||
LogoutResponse,
|
||||
ProfileResponse,
|
||||
UpdateProfileRequest,
|
||||
ChangePasswordRequest,
|
||||
User,
|
||||
AuthState,
|
||||
} from './model/types';
|
||||
|
||||
// API
|
||||
export { userApi } from './api/userApi';
|
||||
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* User Entity Types
|
||||
* API 스펙 기반 타입 정의
|
||||
*/
|
||||
|
||||
// 로그인 요청/응답
|
||||
export interface LoginRequest {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
token: string;
|
||||
userId: number;
|
||||
userName: string;
|
||||
role: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
// 회원가입 요청/응답
|
||||
export interface RegisterRequest {
|
||||
name: string;
|
||||
phoneNumber: string;
|
||||
email: string;
|
||||
password: string;
|
||||
storeName: string;
|
||||
industry?: string;
|
||||
address: string;
|
||||
businessHours?: string;
|
||||
}
|
||||
|
||||
export interface RegisterResponse {
|
||||
token: string;
|
||||
userId: number;
|
||||
userName: string;
|
||||
storeId: number;
|
||||
storeName: string;
|
||||
}
|
||||
|
||||
// 로그아웃 응답
|
||||
export interface LogoutResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
// 프로필 조회/수정
|
||||
export interface ProfileResponse {
|
||||
userId: number;
|
||||
userName: string;
|
||||
phoneNumber: string;
|
||||
email: string;
|
||||
role: string;
|
||||
storeId: number;
|
||||
storeName: string;
|
||||
industry: string;
|
||||
address: string;
|
||||
businessHours: string;
|
||||
createdAt: string;
|
||||
lastLoginAt: string;
|
||||
}
|
||||
|
||||
export interface UpdateProfileRequest {
|
||||
name?: string;
|
||||
phoneNumber?: string;
|
||||
email?: string;
|
||||
storeName?: string;
|
||||
industry?: string;
|
||||
address?: string;
|
||||
businessHours?: string;
|
||||
}
|
||||
|
||||
// 비밀번호 변경
|
||||
export interface ChangePasswordRequest {
|
||||
currentPassword: string;
|
||||
newPassword: string;
|
||||
}
|
||||
|
||||
// User 상태
|
||||
export interface User {
|
||||
userId: number;
|
||||
userName: string;
|
||||
email: string;
|
||||
role: string;
|
||||
phoneNumber?: string;
|
||||
storeId?: number;
|
||||
storeName?: string;
|
||||
industry?: string;
|
||||
address?: string;
|
||||
businessHours?: string;
|
||||
}
|
||||
|
||||
// 인증 상태
|
||||
export interface AuthState {
|
||||
user: User | null;
|
||||
token: string | null;
|
||||
isAuthenticated: boolean;
|
||||
isLoading: boolean;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { useAuth } from './model/useAuth';
|
||||
export { AuthProvider, useAuthContext } from './model/AuthProvider';
|
||||
@@ -0,0 +1,40 @@
|
||||
'use client';
|
||||
|
||||
import React, { createContext, useContext, ReactNode } from 'react';
|
||||
import { useAuth } from './useAuth';
|
||||
import type { AuthState, LoginRequest, RegisterRequest, User } from '@/entities/user';
|
||||
|
||||
interface AuthContextType extends AuthState {
|
||||
login: (credentials: LoginRequest) => Promise<{
|
||||
success: boolean;
|
||||
user?: User;
|
||||
error?: string;
|
||||
}>;
|
||||
register: (data: RegisterRequest) => Promise<{
|
||||
success: boolean;
|
||||
user?: User;
|
||||
error?: string;
|
||||
}>;
|
||||
logout: () => Promise<void>;
|
||||
refreshProfile: () => Promise<{
|
||||
success: boolean;
|
||||
user?: User;
|
||||
error?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | null>(null);
|
||||
|
||||
export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
||||
const auth = useAuth();
|
||||
|
||||
return <AuthContext.Provider value={auth}>{children}</AuthContext.Provider>;
|
||||
};
|
||||
|
||||
export const useAuthContext = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error('useAuthContext must be used within AuthProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -0,0 +1,219 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { userApi } from '@/entities/user';
|
||||
import type {
|
||||
LoginRequest,
|
||||
RegisterRequest,
|
||||
User,
|
||||
AuthState,
|
||||
} from '@/entities/user';
|
||||
|
||||
const TOKEN_KEY = 'accessToken';
|
||||
const USER_KEY = 'user';
|
||||
|
||||
/**
|
||||
* 인증 관련 커스텀 훅
|
||||
*/
|
||||
export const useAuth = () => {
|
||||
const [authState, setAuthState] = useState<AuthState>({
|
||||
user: null,
|
||||
token: null,
|
||||
isAuthenticated: false,
|
||||
isLoading: true,
|
||||
});
|
||||
|
||||
// 초기 인증 상태 확인
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem(TOKEN_KEY);
|
||||
const userStr = localStorage.getItem(USER_KEY);
|
||||
|
||||
if (token && userStr) {
|
||||
try {
|
||||
const user = JSON.parse(userStr) as User;
|
||||
setAuthState({
|
||||
user,
|
||||
token,
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
});
|
||||
} catch {
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
localStorage.removeItem(USER_KEY);
|
||||
setAuthState({
|
||||
user: null,
|
||||
token: null,
|
||||
isAuthenticated: false,
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setAuthState((prev) => ({ ...prev, isLoading: false }));
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 로그인
|
||||
const login = useCallback(async (credentials: LoginRequest) => {
|
||||
try {
|
||||
const response = await userApi.login(credentials);
|
||||
|
||||
const user: User = {
|
||||
userId: response.userId,
|
||||
userName: response.userName,
|
||||
email: response.email,
|
||||
role: response.role,
|
||||
};
|
||||
|
||||
localStorage.setItem(TOKEN_KEY, response.token);
|
||||
localStorage.setItem(USER_KEY, JSON.stringify(user));
|
||||
|
||||
setAuthState({
|
||||
user,
|
||||
token: response.token,
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
return { success: true, user };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '로그인에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 회원가입
|
||||
const register = useCallback(async (data: RegisterRequest) => {
|
||||
console.log('🔐 useAuth.register 시작');
|
||||
console.log('📋 회원가입 데이터:', {
|
||||
...data,
|
||||
password: '***'
|
||||
});
|
||||
|
||||
try {
|
||||
console.log('📡 userApi.register 호출');
|
||||
const response = await userApi.register(data);
|
||||
console.log('📨 userApi.register 응답:', response);
|
||||
|
||||
const user: User = {
|
||||
userId: response.userId,
|
||||
userName: response.userName,
|
||||
email: data.email,
|
||||
role: 'USER',
|
||||
storeId: response.storeId,
|
||||
storeName: response.storeName,
|
||||
};
|
||||
|
||||
console.log('👤 생성된 User 객체:', user);
|
||||
|
||||
localStorage.setItem(TOKEN_KEY, response.token);
|
||||
localStorage.setItem(USER_KEY, JSON.stringify(user));
|
||||
console.log('💾 localStorage에 토큰과 사용자 정보 저장 완료');
|
||||
|
||||
setAuthState({
|
||||
user,
|
||||
token: response.token,
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
});
|
||||
console.log('✅ 인증 상태 업데이트 완료');
|
||||
|
||||
return { success: true, user };
|
||||
} catch (error: any) {
|
||||
console.error('❌ useAuth.register 에러:', error);
|
||||
|
||||
let errorMessage = '회원가입에 실패했습니다.';
|
||||
|
||||
if (error.response) {
|
||||
// 서버가 응답을 반환한 경우
|
||||
console.error('서버 응답 에러:', {
|
||||
status: error.response.status,
|
||||
statusText: error.response.statusText,
|
||||
data: error.response.data,
|
||||
});
|
||||
errorMessage = error.response.data?.message ||
|
||||
error.response.data?.error ||
|
||||
`서버 오류 (${error.response.status})`;
|
||||
} else if (error.request) {
|
||||
// 요청은 보냈지만 응답을 받지 못한 경우
|
||||
console.error('응답 없음:', error.request);
|
||||
errorMessage = '서버로부터 응답이 없습니다. 네트워크 연결을 확인해주세요.';
|
||||
} else {
|
||||
// 요청 설정 중 에러 발생
|
||||
console.error('요청 설정 에러:', error.message);
|
||||
errorMessage = error.message;
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 로그아웃
|
||||
const logout = useCallback(async () => {
|
||||
try {
|
||||
console.log('📡 로그아웃 API 호출');
|
||||
await userApi.logout();
|
||||
console.log('✅ 로그아웃 API 성공');
|
||||
} catch (error: any) {
|
||||
console.warn('⚠️ 로그아웃 API 실패 (서버 에러):', {
|
||||
status: error.response?.status,
|
||||
message: error.response?.data?.message || error.message,
|
||||
});
|
||||
console.log('ℹ️ 로컬 상태는 정리하고 로그아웃 처리를 계속합니다');
|
||||
} finally {
|
||||
console.log('🧹 로컬 토큰 및 사용자 정보 삭제');
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
localStorage.removeItem(USER_KEY);
|
||||
|
||||
setAuthState({
|
||||
user: null,
|
||||
token: null,
|
||||
isAuthenticated: false,
|
||||
isLoading: false,
|
||||
});
|
||||
console.log('✅ 로그아웃 완료 (로컬 상태 정리됨)');
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 프로필 새로고침
|
||||
const refreshProfile = useCallback(async () => {
|
||||
try {
|
||||
const profile = await userApi.getProfile();
|
||||
|
||||
const user: User = {
|
||||
userId: profile.userId,
|
||||
userName: profile.userName,
|
||||
email: profile.email,
|
||||
role: profile.role,
|
||||
phoneNumber: profile.phoneNumber,
|
||||
storeId: profile.storeId,
|
||||
storeName: profile.storeName,
|
||||
industry: profile.industry,
|
||||
address: profile.address,
|
||||
businessHours: profile.businessHours,
|
||||
};
|
||||
|
||||
localStorage.setItem(USER_KEY, JSON.stringify(user));
|
||||
setAuthState((prev) => ({ ...prev, user }));
|
||||
|
||||
return { success: true, user };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '프로필 조회에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
...authState,
|
||||
login,
|
||||
register,
|
||||
logout,
|
||||
refreshProfile,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { useProfile } from './model/useProfile';
|
||||
@@ -0,0 +1,80 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { userApi } from '@/entities/user';
|
||||
import type {
|
||||
ProfileResponse,
|
||||
UpdateProfileRequest,
|
||||
ChangePasswordRequest,
|
||||
} from '@/entities/user';
|
||||
|
||||
/**
|
||||
* 프로필 관련 커스텀 훅
|
||||
*/
|
||||
export const useProfile = () => {
|
||||
const [profile, setProfile] = useState<ProfileResponse | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// 프로필 조회
|
||||
const fetchProfile = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await userApi.getProfile();
|
||||
setProfile(data);
|
||||
return { success: true, data };
|
||||
} catch (err) {
|
||||
const errorMessage =
|
||||
err instanceof Error ? err.message : '프로필 조회에 실패했습니다.';
|
||||
setError(errorMessage);
|
||||
return { success: false, error: errorMessage };
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 프로필 수정
|
||||
const updateProfile = useCallback(async (data: UpdateProfileRequest) => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const updatedProfile = await userApi.updateProfile(data);
|
||||
setProfile(updatedProfile);
|
||||
return { success: true, data: updatedProfile };
|
||||
} catch (err) {
|
||||
const errorMessage =
|
||||
err instanceof Error ? err.message : '프로필 수정에 실패했습니다.';
|
||||
setError(errorMessage);
|
||||
return { success: false, error: errorMessage };
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 비밀번호 변경
|
||||
const changePassword = useCallback(async (data: ChangePasswordRequest) => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
await userApi.changePassword(data);
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
const errorMessage =
|
||||
err instanceof Error ? err.message : '비밀번호 변경에 실패했습니다.';
|
||||
setError(errorMessage);
|
||||
return { success: false, error: errorMessage };
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
profile,
|
||||
isLoading,
|
||||
error,
|
||||
fetchProfile,
|
||||
updateProfile,
|
||||
changePassword,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
import axios, { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from 'axios';
|
||||
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://20.196.65.160:8081';
|
||||
|
||||
export const apiClient: AxiosInstance = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
timeout: 90000, // 30초로 증가
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// Request interceptor - JWT 토큰 추가
|
||||
apiClient.interceptors.request.use(
|
||||
(config: InternalAxiosRequestConfig) => {
|
||||
console.log('🚀 API Request:', {
|
||||
method: config.method?.toUpperCase(),
|
||||
url: config.url,
|
||||
baseURL: config.baseURL,
|
||||
data: config.data,
|
||||
});
|
||||
|
||||
const token = localStorage.getItem('accessToken');
|
||||
if (token && config.headers) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
console.log('🔑 Token added to request');
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error: AxiosError) => {
|
||||
console.error('❌ Request Error:', error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// Response interceptor - 에러 처리
|
||||
apiClient.interceptors.response.use(
|
||||
(response) => {
|
||||
console.log('✅ API Response:', {
|
||||
status: response.status,
|
||||
url: response.config.url,
|
||||
data: response.data,
|
||||
});
|
||||
return response;
|
||||
},
|
||||
(error: AxiosError) => {
|
||||
console.error('❌ API Error:', {
|
||||
message: error.message,
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
url: error.config?.url,
|
||||
data: error.response?.data,
|
||||
});
|
||||
|
||||
if (error.response?.status === 401) {
|
||||
console.warn('🔒 401 Unauthorized - Redirecting to login');
|
||||
// 인증 실패 시 토큰 삭제 및 로그인 페이지로 리다이렉트
|
||||
localStorage.removeItem('accessToken');
|
||||
if (typeof window !== 'undefined') {
|
||||
window.location.href = '/login';
|
||||
}
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default apiClient;
|
||||
@@ -0,0 +1,2 @@
|
||||
export { apiClient } from './client';
|
||||
export type { ApiError } from './types';
|
||||
@@ -0,0 +1,11 @@
|
||||
export interface ApiError {
|
||||
message: string;
|
||||
status: number;
|
||||
code?: string;
|
||||
}
|
||||
|
||||
export interface ApiResponse<T> {
|
||||
data: T;
|
||||
message?: string;
|
||||
success?: boolean;
|
||||
}
|
||||
Reference in New Issue
Block a user