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:
cherry2250
2025-10-28 13:18:23 +09:00
parent a90fd81c46
commit 10c728dbaf
19 changed files with 2126 additions and 87 deletions
+2
View File
@@ -0,0 +1,2 @@
export { useAuth } from './model/useAuth';
export { AuthProvider, useAuthContext } from './model/AuthProvider';
+40
View File
@@ -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;
};
+219
View File
@@ -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,
};
};
+1
View File
@@ -0,0 +1 @@
export { useProfile } from './model/useProfile';
+80
View File
@@ -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,
};
};