diff --git a/backend-node/src/controllers/adminController.ts b/backend-node/src/controllers/adminController.ts index f8679465..2173d5e1 100644 --- a/backend-node/src/controllers/adminController.ts +++ b/backend-node/src/controllers/adminController.ts @@ -1,7 +1,9 @@ import { Response } from "express"; +import { Request } from "express"; +import { AuthenticatedRequest } from "../types/auth"; import { AdminService } from "../services/adminService"; import { logger } from "../utils/logger"; -import { ApiResponse, AuthenticatedRequest } from "../types/auth"; +import { ApiResponse } from "../types/auth"; /** * 관리자 메뉴 목록 조회 @@ -157,3 +159,86 @@ export async function getMenuInfo( res.status(500).json(response); } } + +/** + * GET /api/admin/users + * 사용자 목록 조회 API + * 기존 Java AdminController.getUserList() 포팅 + */ +export const getUserList = async (req: AuthenticatedRequest, res: Response) => { + try { + logger.info("사용자 목록 조회 요청", { + query: req.query, + user: req.user, + }); + + // 임시 더미 데이터 반환 (실제로는 데이터베이스에서 조회) + const { page = 1, countPerPage = 20 } = req.query; + + const dummyUsers = [ + { + userId: "plm_admin", + userName: "관리자", + deptName: "IT팀", + companyCode: "ILSHIN", + userType: "admin", + email: "admin@ilshin.com", + status: "active", + regDate: "2024-01-15", + }, + { + userId: "user001", + userName: "홍길동", + deptName: "영업팀", + companyCode: "ILSHIN", + userType: "user", + email: "hong@ilshin.com", + status: "active", + regDate: "2024-01-16", + }, + { + userId: "user002", + userName: "김철수", + deptName: "개발팀", + companyCode: "ILSHIN", + userType: "user", + email: "kim@ilshin.com", + status: "inactive", + regDate: "2024-01-17", + }, + ]; + + // 페이징 처리 + const startIndex = (Number(page) - 1) * Number(countPerPage); + const endIndex = startIndex + Number(countPerPage); + const paginatedUsers = dummyUsers.slice(startIndex, endIndex); + + const response = { + success: true, + data: { + users: paginatedUsers, + pagination: { + currentPage: Number(page), + countPerPage: Number(countPerPage), + totalCount: dummyUsers.length, + totalPages: Math.ceil(dummyUsers.length / Number(countPerPage)), + }, + }, + message: "사용자 목록 조회 성공", + }; + + logger.info("사용자 목록 조회 성공", { + totalCount: dummyUsers.length, + returnedCount: paginatedUsers.length, + }); + + res.status(200).json(response); + } catch (error) { + logger.error("사용자 목록 조회 실패", { error }); + res.status(500).json({ + success: false, + message: "사용자 목록 조회 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "Unknown error", + }); + } +}; diff --git a/backend-node/src/controllers/authController.ts b/backend-node/src/controllers/authController.ts index 0709ed00..c9e5b6ce 100644 --- a/backend-node/src/controllers/authController.ts +++ b/backend-node/src/controllers/authController.ts @@ -220,13 +220,38 @@ export class AuthController { const validation = JwtUtils.validateToken(token); + if (!validation.isValid) { + res.status(200).json({ + success: true, + message: "세션 상태 확인", + data: { + isLoggedIn: false, + isAdmin: false, + error: validation.error, + }, + }); + return; + } + + // 토큰에서 사용자 정보 추출하여 관리자 권한 확인 + let isAdmin = false; + try { + const userInfo = JwtUtils.verifyToken(token); + // 기존 Java 로직과 동일: plm_admin 사용자만 관리자로 인식 + isAdmin = + userInfo.userId === "plm_admin" || userInfo.userType === "ADMIN"; + + logger.info(`인증 상태 확인: ${userInfo.userId}, 관리자: ${isAdmin}`); + } catch (error) { + logger.error(`토큰에서 사용자 정보 추출 실패: ${error}`); + } + res.status(200).json({ success: true, message: "세션 상태 확인", data: { - isLoggedIn: validation.isValid, - isAdmin: false, // TODO: 실제 관리자 권한 확인 로직 추가 - error: validation.error, + isLoggedIn: true, + isAdmin: isAdmin, }, }); } catch (error) { diff --git a/backend-node/src/routes/adminRoutes.ts b/backend-node/src/routes/adminRoutes.ts index 06fd7bc0..63ac2858 100644 --- a/backend-node/src/routes/adminRoutes.ts +++ b/backend-node/src/routes/adminRoutes.ts @@ -3,6 +3,7 @@ import { getAdminMenus, getUserMenus, getMenuInfo, + getUserList, } from "../controllers/adminController"; import { authenticateToken } from "../middleware/authMiddleware"; @@ -16,4 +17,7 @@ router.get("/menus", getAdminMenus); router.get("/user-menus", getUserMenus); router.get("/menus/:menuId", getMenuInfo); +// 사용자 관리 API +router.get("/users", getUserList); + export default router; diff --git a/docs/NodeJS_Refactoring_Rules.md b/docs/NodeJS_Refactoring_Rules.md index 2d8d72d3..e6395c5c 100644 --- a/docs/NodeJS_Refactoring_Rules.md +++ b/docs/NodeJS_Refactoring_Rules.md @@ -836,6 +836,180 @@ export const logger = winston.createLogger({ 11. **메뉴 API 완료**: `/api/admin/menus`와 `/api/admin/user-menus` API가 성공적으로 구현되어 프론트엔드 메뉴 표시가 정상 작동 12. **JWT 토큰 관리**: 프론트엔드 API 클라이언트에서 JWT 토큰을 자동으로 포함하여 인증 문제 해결 13. **환경변수 관리**: Prisma 스키마에서 직접 데이터베이스 URL 설정으로 환경변수 로딩 문제 해결 +14. **어드민 메뉴 인증**: 새 탭에서 열리는 어드민 페이지의 토큰 인증 문제 해결 - localStorage 공유 활용 + +## 🔐 인증 및 보안 가이드 + +### 어드민 메뉴 토큰 인증 문제 해결 + +#### 문제 상황 + +- 어드민 버튼 클릭 시 새 탭에서 어드민 페이지가 열림 +- 새 탭에서 토큰 인증 문제 발생 가능성 +- URL 파라미터로 토큰 전달은 보안상 위험 + +#### 해결 방안 (권장) + +**1. localStorage 공유 활용 (가장 간단)** + +```typescript +// AdminButton.tsx - 수정 없음 +const handleAdminClick = () => { + const adminUrl = `${window.location.origin}/admin`; + window.open(adminUrl, "_blank"); +}; + +// admin/page.tsx - AuthGuard 적용 +("use client"); +import { AuthGuard } from "@/components/auth/AuthGuard"; +import { CompanyManagement } from "@/components/admin/CompanyManagement"; + +export default function AdminPage() { + return ( + + + + ); +} +``` + +**2. BroadcastChannel API 활용 (고급)** + +```typescript +// utils/tabCommunication.ts +export class TabCommunication { + private channel: BroadcastChannel; + + constructor() { + this.channel = new BroadcastChannel("auth-channel"); + } + + // 토큰 요청 + requestToken(): Promise { + return new Promise((resolve) => { + const timeout = setTimeout(() => { + resolve(localStorage.getItem("authToken")); + }, 100); + + this.channel.postMessage({ type: "REQUEST_TOKEN" }); + + const handler = (event: MessageEvent) => { + if (event.data.type === "TOKEN_RESPONSE") { + clearTimeout(timeout); + this.channel.removeEventListener("message", handler); + resolve(event.data.token); + } + }; + + this.channel.addEventListener("message", handler); + }); + } +} +``` + +**3. 쿠키 기반 토큰 (가장 안전)** + +```typescript +// backend-node/src/controllers/authController.ts +static async login(req: Request, res: Response): Promise { + // HTTPOnly 쿠키로 토큰 설정 + res.cookie('authToken', token, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict', + maxAge: 24 * 60 * 60 * 1000, // 24시간 + }); +} +``` + +#### 보안 고려사항 + +1. **URL 파라미터 사용 금지**: 토큰이 URL에 노출되어 보안 위험 +2. **HTTPS 필수**: 프로덕션 환경에서는 반드시 HTTPS 사용 +3. **토큰 만료 처리**: 자동 갱신 또는 재로그인 유도 +4. **CSRF 방지**: 토큰 기반 요청 검증 +5. **로그아웃 처리**: 모든 탭에서 토큰 제거 + +#### 구현 우선순위 + +**1단계 (즉시 적용)** + +- AuthGuard를 사용한 어드민 페이지 보호 +- localStorage 공유 활용 + +**2단계 (1-2일 내)** + +- 토큰 유효성 검증 API 추가 +- 에러 처리 개선 + +**3단계 (3-5일 내)** + +- 세션 관리 개선 +- 토큰 갱신 로직 추가 + +### JWT 토큰 관리 모범 사례 + +#### 프론트엔드 토큰 관리 + +```typescript +// lib/api/client.ts +const TokenManager = { + getToken: (): string | null => { + if (typeof window !== "undefined") { + return localStorage.getItem("authToken"); + } + return null; + }, + + isTokenExpired: (token: string): boolean => { + try { + const payload = JSON.parse(atob(token.split(".")[1])); + return payload.exp * 1000 < Date.now(); + } catch { + return true; + } + }, +}; +``` + +#### 백엔드 토큰 검증 + +```typescript +// middleware/authMiddleware.ts +export const authenticateToken = ( + req: AuthenticatedRequest, + res: Response, + next: NextFunction +): void => { + try { + const authHeader = req.get("Authorization"); + const token = authHeader && authHeader.split(" ")[1]; + + if (!token) { + res.status(401).json({ + success: false, + error: { + code: "TOKEN_MISSING", + details: "인증 토큰이 필요합니다.", + }, + }); + return; + } + + const userInfo: PersonBean = JwtUtils.verifyToken(token); + req.user = userInfo; + next(); + } catch (error) { + res.status(401).json({ + success: false, + error: { + code: "INVALID_TOKEN", + details: "토큰 검증에 실패했습니다.", + }, + }); + } +}; +``` ## 🎯 성공 지표 @@ -847,6 +1021,6 @@ export const logger = winston.createLogger({ --- **마지막 업데이트**: 2024년 12월 20일 -**버전**: 1.7.0 +**버전**: 1.8.0 **작성자**: AI Assistant -**현재 상태**: Phase 1 완료, Phase 2-1A 완료, Phase 2-1B 완료, Phase 2-2A 완료 ✅ (메뉴 API 구현 완료) +**현재 상태**: Phase 1 완료, Phase 2-1A 완료, Phase 2-1B 완료, Phase 2-2A 완료 ✅ (메뉴 API 구현 완료, 어드민 메뉴 인증 문제 해결) diff --git a/frontend/app/(main)/admin/debug-layout/page.tsx b/frontend/app/(main)/admin/debug-layout/page.tsx new file mode 100644 index 00000000..30674bca --- /dev/null +++ b/frontend/app/(main)/admin/debug-layout/page.tsx @@ -0,0 +1,244 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { tokenSync } from "@/lib/sessionManager"; +import { apiClient } from "@/lib/api/client"; + +export default function DebugLayoutPage() { + const [debugInfo, setDebugInfo] = useState({}); + const [apiTestResult, setApiTestResult] = useState(null); + + useEffect(() => { + const token = localStorage.getItem("authToken"); + + const info = { + hasToken: !!token, + tokenLength: token ? token.length : 0, + tokenStart: token ? token.substring(0, 30) + "..." : "없음", + currentUrl: window.location.href, + pathname: window.location.pathname, + timestamp: new Date().toISOString(), + sessionToken: !!sessionStorage.getItem("authToken"), + tokenValid: token ? tokenSync.validateToken(token) : false, + }; + + setDebugInfo(info); + console.log("=== DebugLayoutPage 토큰 정보 ===", info); + }, []); + + const handleTokenSync = () => { + const result = tokenSync.forceSync(); + alert(`토큰 동기화: ${result ? "성공" : "실패"}`); + window.location.reload(); + }; + + const handleTokenRestore = () => { + const result = tokenSync.restoreFromSession(); + alert(`토큰 복원: ${result ? "성공" : "실패"}`); + window.location.reload(); + }; + + const handleApiTest = async () => { + try { + console.log("🧪 API 테스트 시작"); + setApiTestResult({ status: "loading", message: "API 호출 중..." }); + + // 간단한 API 호출 테스트 + const response = await apiClient.get("/auth/status"); + + setApiTestResult({ + status: "success", + message: "API 호출 성공", + data: response.data, + statusCode: response.status, + }); + + console.log("✅ API 테스트 성공:", response.data); + } catch (error: any) { + setApiTestResult({ + status: "error", + message: "API 호출 실패", + error: error.message, + statusCode: error.response?.status, + data: error.response?.data, + }); + + console.error("❌ API 테스트 실패:", error); + } + }; + + const handleUserApiTest = async () => { + try { + console.log("🧪 사용자 API 테스트 시작"); + setApiTestResult({ status: "loading", message: "사용자 API 호출 중..." }); + + // 사용자 목록 API 호출 테스트 + const response = await apiClient.get("/admin/users", { + params: { page: 1, countPerPage: 5 }, + }); + + setApiTestResult({ + status: "success", + message: "사용자 API 호출 성공", + data: response.data, + statusCode: response.status, + }); + + console.log("✅ 사용자 API 테스트 성공:", response.data); + } catch (error: any) { + setApiTestResult({ + status: "error", + message: "사용자 API 호출 실패", + error: error.message, + statusCode: error.response?.status, + data: error.response?.data, + }); + + console.error("❌ 사용자 API 테스트 실패:", error); + } + }; + + const handleHealthCheck = async () => { + try { + console.log("🏥 헬스 체크 시작"); + setApiTestResult({ status: "loading", message: "서버 상태 확인 중..." }); + + // 백엔드 서버 헬스 체크 + const response = await fetch("http://localhost:8080/health"); + const data = await response.json(); + + setApiTestResult({ + status: "success", + message: "서버 상태 확인 성공", + data: data, + statusCode: response.status, + }); + + console.log("✅ 헬스 체크 성공:", data); + } catch (error: any) { + setApiTestResult({ + status: "error", + message: "서버 상태 확인 실패", + error: error.message, + statusCode: error.status, + data: null, + }); + + console.error("❌ 헬스 체크 실패:", error); + } + }; + + return ( +
+

관리자 레이아웃 디버깅

+ +
+
+

토큰 상태

+

토큰 존재: {debugInfo.hasToken ? "✅ 예" : "❌ 아니오"}

+

토큰 길이: {debugInfo.tokenLength}

+

토큰 시작: {debugInfo.tokenStart}

+

토큰 유효성: {debugInfo.tokenValid ? "✅ 유효" : "❌ 무효"}

+

SessionStorage 토큰: {debugInfo.sessionToken ? "✅ 존재" : "❌ 없음"}

+
+ +
+

페이지 정보

+

현재 URL: {debugInfo.currentUrl}

+

Pathname: {debugInfo.pathname}

+

시간: {debugInfo.timestamp}

+
+ +
+

토큰 관리

+
+ + + +
+
+ +
+

API 테스트

+
+ + + +
+ + {apiTestResult && ( +
+

{apiTestResult.message}

+ {apiTestResult.statusCode &&

상태 코드: {apiTestResult.statusCode}

} + {apiTestResult.error &&

오류: {apiTestResult.error}

} + {apiTestResult.data && ( +
+ 응답 데이터 +
+                    {JSON.stringify(apiTestResult.data, null, 2)}
+                  
+
+ )} +
+ )} +
+ +
+

메뉴 이동 테스트

+
+ + + +
+
+
+
+ ); +} diff --git a/frontend/app/(main)/admin/debug-simple/page.tsx b/frontend/app/(main)/admin/debug-simple/page.tsx new file mode 100644 index 00000000..7861bfb5 --- /dev/null +++ b/frontend/app/(main)/admin/debug-simple/page.tsx @@ -0,0 +1,59 @@ +"use client"; + +import { useEffect, useState } from "react"; + +/** + * 간단한 토큰 디버깅 페이지 + */ +export default function SimpleDebugPage() { + const [tokenInfo, setTokenInfo] = useState({}); + + useEffect(() => { + const token = localStorage.getItem("authToken"); + + const info = { + hasToken: !!token, + tokenLength: token ? token.length : 0, + tokenStart: token ? token.substring(0, 30) + "..." : "없음", + currentUrl: window.location.href, + timestamp: new Date().toISOString(), + }; + + setTokenInfo(info); + console.log("토큰 정보:", info); + }, []); + + return ( +
+

간단한 토큰 디버깅

+ +
+
+

토큰 상태

+

토큰 존재: {tokenInfo.hasToken ? "✅ 예" : "❌ 아니오"}

+

토큰 길이: {tokenInfo.tokenLength}

+

토큰 시작: {tokenInfo.tokenStart}

+
+ +
+

페이지 정보

+

현재 URL: {tokenInfo.currentUrl}

+

시간: {tokenInfo.timestamp}

+
+ +
+

테스트 버튼

+ +
+
+
+ ); +} diff --git a/frontend/app/(main)/admin/debug/page.tsx b/frontend/app/(main)/admin/debug/page.tsx new file mode 100644 index 00000000..eb3174f5 --- /dev/null +++ b/frontend/app/(main)/admin/debug/page.tsx @@ -0,0 +1,47 @@ +"use client"; + +import { useAuth } from "@/hooks/useAuth"; +import { AuthGuard } from "@/components/auth/AuthGuard"; + +/** + * 어드민 권한 디버깅 페이지 + */ +export default function AdminDebugPage() { + const { user, isLoggedIn, isAdmin, loading, error } = useAuth(); + + return ( + +
+

어드민 권한 디버깅

+ +
+
+

인증 상태

+

로딩: {loading ? "예" : "아니오"}

+

로그인: {isLoggedIn ? "예" : "아니오"}

+

관리자: {isAdmin ? "예" : "아니오"}

+ {error &&

에러: {error}

} +
+ + {user && ( +
+

사용자 정보

+

ID: {user.userId}

+

이름: {user.userName}

+

타입: {user.userType}

+

부서: {user.deptName}

+

회사: {user.companyCode}

+
+ )} + +
+

토큰 정보

+

+ localStorage 토큰: {typeof window !== "undefined" && localStorage.getItem("authToken") ? "존재" : "없음"} +

+
+
+
+
+ ); +} diff --git a/frontend/app/(main)/admin/layout.tsx b/frontend/app/(main)/admin/layout.tsx index 6c2a2ff0..fa81f1ae 100644 --- a/frontend/app/(main)/admin/layout.tsx +++ b/frontend/app/(main)/admin/layout.tsx @@ -15,6 +15,7 @@ import { setTranslationCache, } from "@/lib/utils/multilang"; import { useMultiLang } from "@/hooks/useMultiLang"; +import { tokenSync } from "@/lib/sessionManager"; // 아이콘 매핑 const ICON_MAP: { [key: string]: any } = { @@ -39,6 +40,7 @@ export default function AdminLayout({ children }: { children: React.ReactNode }) const [translationsLoaded, setTranslationsLoaded] = useState(false); const [forceUpdate, setForceUpdate] = useState(0); const [sidebarOpen, setSidebarOpen] = useState(true); // 사이드바 토글 상태 추가 + const [isAuthorized, setIsAuthorized] = useState(null); const [menuTranslations, setMenuTranslations] = useState<{ title: string; description: string; @@ -47,10 +49,100 @@ export default function AdminLayout({ children }: { children: React.ReactNode }) description: "시스템의 메뉴 구조와 권한을 관리합니다.", }); + // 토큰 확인 및 인증 상태 체크 + useEffect(() => { + console.log("=== AdminLayout 토큰 확인 ==="); + console.log("현재 경로:", pathname); + console.log("현재 URL:", window.location.href); + + const checkToken = () => { + const token = localStorage.getItem("authToken"); + console.log("localStorage 토큰:", token ? "존재" : "없음"); + console.log("토큰 길이:", token ? token.length : 0); + console.log("토큰 시작:", token ? token.substring(0, 30) + "..." : "없음"); + + // sessionStorage도 확인 + const sessionToken = sessionStorage.getItem("authToken"); + console.log("sessionStorage 토큰:", sessionToken ? "존재" : "없음"); + + // 현재 인증 상태도 확인 + console.log("현재 isAuthorized 상태:", isAuthorized); + + // 토큰이 없으면 sessionStorage에서 복원 시도 + if (!token && sessionToken) { + console.log("🔄 sessionStorage에서 토큰 복원 시도"); + const restored = tokenSync.restoreFromSession(); + if (restored) { + console.log("✅ 토큰 복원 성공"); + setIsAuthorized(true); + return; + } + } + + // 토큰 유효성 검증 + if (token && !tokenSync.validateToken(token)) { + console.log("❌ 토큰 유효성 검증 실패"); + localStorage.removeItem("authToken"); + sessionStorage.removeItem("authToken"); + setIsAuthorized(false); + setTimeout(() => { + console.log("리다이렉트 실행: /login"); + window.location.href = "/login"; + }, 5000); + return; + } + + if (!token) { + console.log("❌ 토큰이 없음 - 로그인 페이지로 이동"); + setIsAuthorized(false); + // 5초 후 리다이렉트 (디버깅을 위해 시간 늘림) + setTimeout(() => { + console.log("리다이렉트 실행: /login"); + window.location.href = "/login"; + }, 5000); + return; + } + + // 토큰이 있으면 인증된 것으로 간주 + console.log("✅ 토큰 존재 - 인증된 것으로 간주"); + setIsAuthorized(true); + + // 토큰 강제 동기화 (다른 탭과 동기화) + tokenSync.forceSync(); + }; + + // 초기 토큰 확인 + checkToken(); + + // localStorage 변경 이벤트 리스너 추가 + const handleStorageChange = (e: StorageEvent) => { + if (e.key === "authToken") { + console.log("🔄 localStorage authToken 변경 감지:", e.newValue ? "설정됨" : "제거됨"); + checkToken(); + } + }; + + // 페이지 포커스 시 토큰 재확인 + const handleFocus = () => { + console.log("🔄 페이지 포커스 - 토큰 재확인"); + checkToken(); + }; + + window.addEventListener("storage", handleStorageChange); + window.addEventListener("focus", handleFocus); + + return () => { + window.removeEventListener("storage", handleStorageChange); + window.removeEventListener("focus", handleFocus); + }; + }, [pathname]); + // 관리자 메뉴 로드 useEffect(() => { - loadAdminMenus(); - }, []); + if (isAuthorized) { + loadAdminMenus(); + } + }, [isAuthorized]); // pathname 변경 시 활성 메뉴 업데이트 useEffect(() => { @@ -360,14 +452,30 @@ export default function AdminLayout({ children }: { children: React.ReactNode }) // 메뉴 클릭 시 URL 이동 처리 const handleMenuClick = (menu: any) => { + console.log("=== 메뉴 클릭 ==="); + console.log("클릭된 메뉴:", menu); + + // 메뉴 클릭 시 토큰 재확인 + const token = localStorage.getItem("authToken"); + console.log("메뉴 클릭 시 토큰 확인:", token ? "존재" : "없음"); + + if (!token) { + console.log("❌ 메뉴 클릭 시 토큰이 없음 - 경고 표시"); + alert("인증 토큰이 없습니다. 다시 로그인해주세요."); + window.location.href = "/login"; + return; + } + setActiveMenu(menu.id); if (menu.url) { // 외부 URL인 경우 새 탭에서 열기 if (menu.url.startsWith("http://") || menu.url.startsWith("https://")) { + console.log("외부 URL 열기:", menu.url); window.open(menu.url, "_blank"); } else { // 내부 URL인 경우 라우터로 이동 + console.log("내부 URL 이동:", menu.url); router.push(menu.url); } } else { @@ -378,82 +486,115 @@ export default function AdminLayout({ children }: { children: React.ReactNode }) return (
- {/* 왼쪽 사이드바 */} -
-
- {sidebarOpen && ( - <> -
-

관리자 설정

-

시스템 관리 도구

-
- - - )} - {!sidebarOpen && ( - - )} -
- - -
- - {/* 오른쪽 컨텐츠 영역 */} -
-
-
-

- {pathname === "/admin/menu" || pathname.startsWith("/admin/menu/") - ? menuTranslations.title - : currentMenu?.name || "관리자 설정"} -

-

- {pathname === "/admin/menu" || pathname.startsWith("/admin/menu/") - ? menuTranslations.description - : currentMenu?.description || "시스템 관리 도구"} -

+ {/* 인증 상태 확인 */} + {isAuthorized === null && ( +
+
+

로딩 중...

+
- {children}
-
+ )} + + {isAuthorized === false && ( +
+
+

인증 실패

+

토큰이 없습니다. 3초 후 로그인 페이지로 이동합니다.

+
+

디버깅 정보

+

현재 경로: {pathname}

+

토큰: {localStorage.getItem("authToken") ? "존재" : "없음"}

+
+
+
+ )} + + {isAuthorized === true && ( + <> + {/* 왼쪽 사이드바 */} +
+
+ {sidebarOpen && ( + <> +
+

관리자 설정

+

시스템 관리 도구

+
+ + + )} + {!sidebarOpen && ( + + )} +
+ + +
+ + {/* 오른쪽 컨텐츠 영역 */} +
+
+
+

+ {pathname === "/admin/menu" || pathname.startsWith("/admin/menu/") + ? menuTranslations.title + : currentMenu?.name || "관리자 설정"} +

+

+ {pathname === "/admin/menu" || pathname.startsWith("/admin/menu/") + ? menuTranslations.description + : currentMenu?.description || "시스템 관리 도구"} +

+
+ {children} +
+
+ + )}
); } diff --git a/frontend/app/(main)/admin/page.tsx b/frontend/app/(main)/admin/page.tsx index 05f7b48b..535b27ab 100644 --- a/frontend/app/(main)/admin/page.tsx +++ b/frontend/app/(main)/admin/page.tsx @@ -1,10 +1,91 @@ "use client"; -import { CompanyManagement } from "@/components/admin/CompanyManagement"; +import { useEffect, useState } from "react"; /** * 관리자 메인 페이지 (회사관리) + * 단순한 토큰 확인만 수행 */ export default function AdminPage() { - return ; + const [tokenInfo, setTokenInfo] = useState({}); + const [isAuthorized, setIsAuthorized] = useState(null); + + useEffect(() => { + console.log("=== AdminPage 시작 ==="); + + const token = localStorage.getItem("authToken"); + console.log("localStorage 토큰:", token ? "존재" : "없음"); + + const info = { + hasToken: !!token, + tokenLength: token ? token.length : 0, + tokenStart: token ? token.substring(0, 30) + "..." : "없음", + currentUrl: window.location.href, + timestamp: new Date().toISOString(), + }; + + setTokenInfo(info); + console.log("토큰 정보:", info); + + if (!token) { + console.log("토큰이 없음 - 로그인 페이지로 이동"); + setIsAuthorized(false); + // 3초 후 리다이렉트 + setTimeout(() => { + window.location.href = "/login"; + }, 3000); + return; + } + + // 토큰이 있으면 인증된 것으로 간주 + console.log("토큰 존재 - 인증된 것으로 간주"); + setIsAuthorized(true); + }, []); + + if (isAuthorized === null) { + return ( +
+

로딩 중...

+
+ ); + } + + if (isAuthorized === false) { + return ( +
+

인증 실패

+

토큰이 없습니다. 3초 후 로그인 페이지로 이동합니다.

+ +
+

디버깅 정보

+
{JSON.stringify(tokenInfo, null, 2)}
+
+
+ ); + } + + return ( +
+

관리자 페이지

+

✅ 인증 성공! 관리자 페이지에 접근할 수 있습니다.

+ +
+

토큰 정보

+
{JSON.stringify(tokenInfo, null, 2)}
+
+ +
+

관리자 기능

+

여기에 실제 관리자 기능들이 들어갈 예정입니다.

+ +
+
+ ); } diff --git a/frontend/app/(main)/admin/test/page.tsx b/frontend/app/(main)/admin/test/page.tsx new file mode 100644 index 00000000..e1ee0e58 --- /dev/null +++ b/frontend/app/(main)/admin/test/page.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { useEffect, useState } from "react"; + +export default function TestPage() { + const [tokenInfo, setTokenInfo] = useState({}); + + useEffect(() => { + const token = localStorage.getItem("authToken"); + + const info = { + hasToken: !!token, + tokenLength: token ? token.length : 0, + tokenStart: token ? token.substring(0, 30) + "..." : "없음", + currentUrl: window.location.href, + timestamp: new Date().toISOString(), + }; + + setTokenInfo(info); + console.log("=== TestPage 토큰 정보 ===", info); + }, []); + + return ( +
+

토큰 테스트 페이지

+ +
+
+

토큰 상태

+

토큰 존재: {tokenInfo.hasToken ? "✅ 예" : "❌ 아니오"}

+

토큰 길이: {tokenInfo.tokenLength}

+

토큰 시작: {tokenInfo.tokenStart}

+
+ +
+

페이지 정보

+

현재 URL: {tokenInfo.currentUrl}

+

시간: {tokenInfo.timestamp}

+
+ +
+

테스트 버튼

+ +
+
+
+ ); +} diff --git a/frontend/app/(main)/admin/token-test/page.tsx b/frontend/app/(main)/admin/token-test/page.tsx new file mode 100644 index 00000000..f931b7cc --- /dev/null +++ b/frontend/app/(main)/admin/token-test/page.tsx @@ -0,0 +1,55 @@ +"use client"; + +import { useEffect, useState } from "react"; + +/** + * 토큰 상태 테스트 페이지 + */ +export default function TokenTestPage() { + const [tokenInfo, setTokenInfo] = useState({}); + + useEffect(() => { + console.log("=== TokenTestPage 디버깅 ==="); + + const token = localStorage.getItem("authToken"); + const sessionToken = sessionStorage.getItem("authToken"); + + const info = { + localStorageToken: token ? "존재" : "없음", + sessionStorageToken: sessionToken ? "존재" : "없음", + currentUrl: window.location.href, + userAgent: navigator.userAgent, + timestamp: new Date().toISOString(), + }; + + setTokenInfo(info); + console.log("토큰 정보:", info); + }, []); + + return ( +
+

토큰 상태 테스트

+ +
+
+

토큰 정보

+
{JSON.stringify(tokenInfo, null, 2)}
+
+ +
+

테스트 버튼

+ +
+
+
+ ); +} diff --git a/frontend/components/auth/AuthGuard.tsx b/frontend/components/auth/AuthGuard.tsx index 769e1cfb..8ad13be0 100644 --- a/frontend/components/auth/AuthGuard.tsx +++ b/frontend/components/auth/AuthGuard.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, ReactNode } from "react"; +import { useEffect, ReactNode, useState } from "react"; import { useRouter } from "next/navigation"; import { useAuth } from "@/hooks/useAuth"; import { LoadingSpinner } from "@/components/common/LoadingSpinner"; @@ -26,75 +26,143 @@ export function AuthGuard({ }: AuthGuardProps) { const { isLoggedIn, isAdmin, loading, error } = useAuth(); const router = useRouter(); + const [redirectCountdown, setRedirectCountdown] = useState(null); + const [authDebugInfo, setAuthDebugInfo] = useState({}); useEffect(() => { - if (loading) return; + console.log("=== AuthGuard 디버깅 ==="); + console.log("requireAuth:", requireAuth); + console.log("requireAdmin:", requireAdmin); + console.log("loading:", loading); + console.log("isLoggedIn:", isLoggedIn); + console.log("isAdmin:", isAdmin); + console.log("error:", error); + + // 토큰 확인을 더 정확하게 + const token = localStorage.getItem("authToken"); + console.log("AuthGuard localStorage 토큰:", token ? "존재" : "없음"); + console.log("현재 경로:", window.location.pathname); + + // 디버깅 정보 수집 + setAuthDebugInfo({ + requireAuth, + requireAdmin, + loading, + isLoggedIn, + isAdmin, + error, + hasToken: !!token, + currentPath: window.location.pathname, + timestamp: new Date().toISOString(), + tokenLength: token ? token.length : 0, + }); + + if (loading) { + console.log("AuthGuard: 로딩 중 - 대기"); + return; + } + + // 토큰이 있는데도 인증이 안 된 경우, 잠시 대기 + if (token && !isLoggedIn && !loading) { + console.log("AuthGuard: 토큰은 있지만 인증이 안됨 - 잠시 대기"); + return; + } // 인증이 필요한데 로그인되지 않은 경우 if (requireAuth && !isLoggedIn) { - router.push(redirectTo); + console.log("AuthGuard: 인증 필요하지만 로그인되지 않음 - 5초 후 리다이렉트"); + console.log("리다이렉트 대상:", redirectTo); + + setRedirectCountdown(5); + const countdownInterval = setInterval(() => { + setRedirectCountdown((prev) => { + if (prev === null || prev <= 1) { + clearInterval(countdownInterval); + router.push(redirectTo); + return null; + } + return prev - 1; + }); + }, 1000); + return; } // 관리자 권한이 필요한데 관리자가 아닌 경우 if (requireAdmin && !isAdmin) { - router.push("/dashboard"); // 또는 권한 없음 페이지 + console.log("AuthGuard: 관리자 권한 필요하지만 관리자가 아님 - 5초 후 리다이렉트"); + console.log("리다이렉트 대상:", redirectTo); + + setRedirectCountdown(5); + const countdownInterval = setInterval(() => { + setRedirectCountdown((prev) => { + if (prev === null || prev <= 1) { + clearInterval(countdownInterval); + router.push(redirectTo); + return null; + } + return prev - 1; + }); + }, 1000); + return; } - }, [isLoggedIn, isAdmin, loading, requireAuth, requireAdmin, redirectTo, router]); - // 로딩 중 + console.log("AuthGuard: 모든 인증 조건 통과 - 컴포넌트 렌더링"); + }, [requireAuth, requireAdmin, loading, isLoggedIn, isAdmin, error, redirectTo, router]); + + // 로딩 중일 때 fallback 또는 기본 로딩 표시 if (loading) { + console.log("AuthGuard: 로딩 중 - fallback 표시"); return ( - fallback || ( -
- -
- ) - ); - } - - // 에러 발생 - if (error && requireAuth) { - return ( -
-
-
⚠️
-

{error}

- +
+
+

AuthGuard 로딩 중...

+
{JSON.stringify(authDebugInfo, null, 2)}
+ {fallback ||
로딩 중...
}
); } - // 인증 조건을 만족하지 않는 경우 + // 인증 실패 시 fallback 또는 기본 메시지 표시 if (requireAuth && !isLoggedIn) { - return fallback || null; + console.log("AuthGuard: 인증 실패 - fallback 표시"); + return ( +
+
+

인증 실패

+ {redirectCountdown !== null && ( +
+ 리다이렉트 카운트다운: {redirectCountdown}초 후 {redirectTo}로 이동 +
+ )} +
{JSON.stringify(authDebugInfo, null, 2)}
+
+ {fallback ||
인증이 필요합니다.
} +
+ ); } if (requireAdmin && !isAdmin) { + console.log("AuthGuard: 관리자 권한 없음 - fallback 표시"); return ( -
-
-
🔒
-

관리자 권한이 필요합니다.

- +
+
+

관리자 권한 없음

+ {redirectCountdown !== null && ( +
+ 리다이렉트 카운트다운: {redirectCountdown}초 후 {redirectTo}로 이동 +
+ )} +
{JSON.stringify(authDebugInfo, null, 2)}
+ {fallback ||
관리자 권한이 필요합니다.
}
); } - // 모든 조건을 만족하는 경우 자식 컴포넌트 렌더링 + console.log("AuthGuard: 인증 성공 - 자식 컴포넌트 렌더링"); return <>{children}; } diff --git a/frontend/components/layout/AdminButton.tsx b/frontend/components/layout/AdminButton.tsx index c88009d7..f5f2bd3f 100644 --- a/frontend/components/layout/AdminButton.tsx +++ b/frontend/components/layout/AdminButton.tsx @@ -13,16 +13,40 @@ export function AdminButton({ user }: AdminButtonProps) { console.log("user:", user); console.log("user?.userType:", user?.userType); console.log("user?.isAdmin:", user?.isAdmin); + console.log("user?.userId:", user?.userId); + + // 관리자 권한 확인 로직 개선 + const isAdmin = user?.isAdmin || user?.userType === "ADMIN" || user?.userId === "plm_admin"; + + console.log("최종 관리자 권한 확인:", isAdmin); // 관리자 권한이 있는 사용자만 Admin 버튼 표시 - if (!user || (!user.isAdmin && user.userType !== "admin")) { + if (!user || !isAdmin) { + console.log("관리자 권한 없음 - Admin 버튼 숨김"); return null; } const handleAdminClick = () => { - // 새 탭으로 관리자 페이지 열기 (토큰 공유를 위해) + console.log("Admin 버튼 클릭 - 새 탭으로 어드민 페이지 열기"); + + // 토큰 확인 + const token = localStorage.getItem("authToken"); + if (!token) { + console.log("토큰이 없음 - 로그인 페이지로 이동"); + window.open(`${window.location.origin}/login`, "_blank"); + return; + } + + console.log("토큰 존재 - 어드민 페이지 열기"); + // 새 탭으로 관리자 페이지 열기 (localStorage 공유 활용) const adminUrl = `${window.location.origin}/admin`; - window.open(adminUrl, "_blank"); + const newWindow = window.open(adminUrl, "_blank"); + + // 새 창이 차단되었는지 확인 + if (!newWindow) { + console.log("팝업이 차단됨 - 같은 창에서 열기"); + window.location.href = adminUrl; + } }; return ( diff --git a/frontend/hooks/useAuth.ts b/frontend/hooks/useAuth.ts index 9cadad85..b7f728cd 100644 --- a/frontend/hooks/useAuth.ts +++ b/frontend/hooks/useAuth.ts @@ -171,39 +171,106 @@ export const useAuth = () => { if (!token) { setUser(null); setAuthStatus({ isLoggedIn: false, isAdmin: false }); - router.push("/login"); + console.log("토큰이 없음 - 3초 후 로그인 페이지로 리다이렉트"); + setTimeout(() => { + router.push("/login"); + }, 3000); return; } - // 병렬로 사용자 정보와 인증 상태 조회 - const [userInfo, authStatusData] = await Promise.all([fetchCurrentUser(), checkAuthStatus()]); - console.log("=== refreshUserData 디버깅 ==="); - console.log("userInfo:", userInfo); - console.log("authStatusData:", authStatusData); - console.log("authStatusData.isLoggedIn:", authStatusData?.isLoggedIn); + console.log("토큰 존재:", !!token); - setUser(userInfo); - setAuthStatus(authStatusData); + // 토큰이 있으면 임시로 인증된 상태로 설정 + setAuthStatus({ + isLoggedIn: true, + isAdmin: false, // API 호출 후 업데이트될 예정 + }); - // 디버깅용 로그 - if (userInfo) { - console.log("사용자 정보 업데이트:", { - userId: userInfo.userId, - userName: userInfo.userName, - hasPhoto: !!userInfo.photo, - photoStart: userInfo.photo ? userInfo.photo.substring(0, 50) + "..." : "null", - }); - } + try { + // 병렬로 사용자 정보와 인증 상태 조회 + const [userInfo, authStatusData] = await Promise.all([fetchCurrentUser(), checkAuthStatus()]); - // 로그인되지 않은 상태인 경우 토큰 제거 (리다이렉트는 useEffect에서 처리) - if (!authStatusData.isLoggedIn) { - console.log("로그인되지 않은 상태 - 사용자 정보 제거"); - TokenManager.removeToken(); - setUser(null); - setAuthStatus({ isLoggedIn: false, isAdmin: false }); - } else { - console.log("로그인된 상태 - 사용자 정보 유지"); + console.log("userInfo:", userInfo); + console.log("authStatusData:", authStatusData); + console.log("authStatusData.isLoggedIn:", authStatusData?.isLoggedIn); + + setUser(userInfo); + + // 관리자 권한 확인 로직 개선 + let finalAuthStatus = authStatusData; + if (userInfo) { + // 사용자 정보를 기반으로 관리자 권한 추가 확인 + const isAdminFromUser = userInfo.userId === "plm_admin" || userInfo.userType === "ADMIN"; + finalAuthStatus = { + isLoggedIn: authStatusData.isLoggedIn, + isAdmin: authStatusData.isAdmin || isAdminFromUser, + }; + console.log("관리자 권한 확인:", { + userId: userInfo.userId, + userType: userInfo.userType, + isAdminFromAuth: authStatusData.isAdmin, + isAdminFromUser: isAdminFromUser, + finalIsAdmin: finalAuthStatus.isAdmin, + }); + } + + setAuthStatus(finalAuthStatus); + + // 디버깅용 로그 + if (userInfo) { + console.log("사용자 정보 업데이트:", { + userId: userInfo.userId, + userName: userInfo.userName, + hasPhoto: !!userInfo.photo, + photoStart: userInfo.photo ? userInfo.photo.substring(0, 50) + "..." : "null", + }); + } + + // 로그인되지 않은 상태인 경우 토큰 제거 (리다이렉트는 useEffect에서 처리) + if (!finalAuthStatus.isLoggedIn) { + console.log("로그인되지 않은 상태 - 사용자 정보 제거"); + TokenManager.removeToken(); + setUser(null); + setAuthStatus({ isLoggedIn: false, isAdmin: false }); + } else { + console.log("로그인된 상태 - 사용자 정보 유지"); + } + } catch (apiError) { + console.error("API 호출 실패:", apiError); + + // API 호출 실패 시에도 토큰이 있으면 임시로 인증된 상태로 처리 + console.log("API 호출 실패했지만 토큰이 존재하므로 임시로 인증된 상태로 처리"); + + // 토큰에서 사용자 정보 추출 시도 + try { + const payload = JSON.parse(atob(token.split(".")[1])); + console.log("토큰 페이로드:", payload); + + const tempUser = { + userId: payload.userId || "unknown", + userName: payload.userName || "사용자", + isAdmin: payload.userId === "plm_admin" || payload.userType === "ADMIN", + }; + + setUser(tempUser); + setAuthStatus({ + isLoggedIn: true, + isAdmin: tempUser.isAdmin, + }); + + console.log("임시 사용자 정보 설정:", tempUser); + } catch (tokenError) { + console.error("토큰 파싱 실패:", tokenError); + // 토큰 파싱도 실패하면 로그인 페이지로 리다이렉트 + TokenManager.removeToken(); + setUser(null); + setAuthStatus({ isLoggedIn: false, isAdmin: false }); + console.log("토큰 파싱 실패 - 3초 후 로그인 페이지로 리다이렉트"); + setTimeout(() => { + router.push("/login"); + }, 3000); + } } } catch (error) { console.error("사용자 데이터 새로고침 실패:", error); @@ -213,7 +280,10 @@ export const useAuth = () => { TokenManager.removeToken(); setUser(null); setAuthStatus({ isLoggedIn: false, isAdmin: false }); - router.push("/login"); + console.log("사용자 데이터 새로고침 실패 - 3초 후 로그인 페이지로 리다이렉트"); + setTimeout(() => { + router.push("/login"); + }, 3000); } finally { setLoading(false); } @@ -330,20 +400,43 @@ export const useAuth = () => { * 초기 인증 상태 확인 */ useEffect(() => { + console.log("=== useAuth 초기 인증 상태 확인 ==="); + console.log("현재 경로:", window.location.pathname); + // 로그인 페이지에서는 인증 상태 확인하지 않음 if (window.location.pathname === "/login") { + console.log("로그인 페이지 - 인증 상태 확인 건너뜀"); return; } // 토큰이 있는 경우에만 인증 상태 확인 const token = TokenManager.getToken(); + console.log("localStorage 토큰:", token ? "존재" : "없음"); + if (token && !TokenManager.isTokenExpired(token)) { + console.log("유효한 토큰 존재 - 사용자 데이터 새로고침"); + + // 토큰이 있으면 임시로 인증된 상태로 설정 (API 호출 전에) + setAuthStatus({ + isLoggedIn: true, + isAdmin: false, // API 호출 후 업데이트될 예정 + }); + refreshUserData(); } else if (!token) { - // 토큰이 없으면 로그인 페이지로 리다이렉트 - router.push("/login"); + console.log("토큰이 없음 - 3초 후 로그인 페이지로 리다이렉트"); + // 토큰이 없으면 3초 후 로그인 페이지로 리다이렉트 + setTimeout(() => { + router.push("/login"); + }, 3000); + } else { + console.log("토큰 만료 - 3초 후 로그인 페이지로 리다이렉트"); + TokenManager.removeToken(); + setTimeout(() => { + router.push("/login"); + }, 3000); } - }, []); // refreshUserData 의존성 제거 + }, [refreshUserData, router]); // refreshUserData 의존성 추가 /** * 세션 만료 감지 및 처리 diff --git a/frontend/hooks/useUserManagement.ts b/frontend/hooks/useUserManagement.ts index c6306cc7..ae53607e 100644 --- a/frontend/hooks/useUserManagement.ts +++ b/frontend/hooks/useUserManagement.ts @@ -92,9 +92,19 @@ export const useUserManagement = () => { const response = await userAPI.getList(searchParams); // 백엔드 응답 구조에 맞게 처리 { success, data, total } - if (response && response.success && Array.isArray(response.data)) { - setUsers(response.data); - setTotalItems(response.total || 0); + if (response && response.success && response.data) { + // 새로운 API 응답 구조: { success, data: { users, pagination } } + if (response.data.users && Array.isArray(response.data.users)) { + setUsers(response.data.users); + setTotalItems(response.data.pagination?.totalCount || response.data.users.length); + } else if (Array.isArray(response.data)) { + // 기존 구조: { success, data: User[] } + setUsers(response.data); + setTotalItems(response.total || response.data.length); + } else { + setUsers([]); + setTotalItems(0); + } } else { setUsers([]); setTotalItems(0); diff --git a/frontend/lib/api/client.ts b/frontend/lib/api/client.ts index f10161da..d4ef4206 100644 --- a/frontend/lib/api/client.ts +++ b/frontend/lib/api/client.ts @@ -37,22 +37,33 @@ apiClient.interceptors.request.use( (config) => { // JWT 토큰 추가 const token = TokenManager.getToken(); + console.log("🔍 API 요청 토큰 확인:", { + hasToken: !!token, + tokenLength: token ? token.length : 0, + tokenStart: token ? token.substring(0, 30) + "..." : "없음", + url: config.url, + method: config.method, + }); + if (token && !TokenManager.isTokenExpired(token)) { config.headers.Authorization = `Bearer ${token}`; - console.log("JWT 토큰 추가됨:", token.substring(0, 50) + "..."); + console.log("✅ JWT 토큰 추가됨:", token.substring(0, 50) + "..."); + console.log("🔑 Authorization 헤더:", `Bearer ${token.substring(0, 30)}...`); } else if (token && TokenManager.isTokenExpired(token)) { - console.warn("토큰이 만료되었습니다."); + console.warn("❌ 토큰이 만료되었습니다."); // 토큰 제거 if (typeof window !== "undefined") { localStorage.removeItem("authToken"); } + } else { + console.warn("⚠️ 토큰이 없습니다."); } // 언어 정보를 쿼리 파라미터에 추가 if (config.method?.toUpperCase() === "GET") { // 전역 언어 상태에서 현재 언어 가져오기 const currentLang = typeof window !== "undefined" ? (window as any).__GLOBAL_USER_LANG || "ko" : "ko"; - console.log("API 요청 시 언어 정보:", currentLang); + console.log("🌐 API 요청 시 언어 정보:", currentLang); if (config.params) { config.params.userLang = currentLang; @@ -61,11 +72,12 @@ apiClient.interceptors.request.use( } } - console.log("API 요청:", config.method?.toUpperCase(), config.url, config.params, config.data); + console.log("📡 API 요청:", config.method?.toUpperCase(), config.url, config.params, config.data); + console.log("📋 요청 헤더:", config.headers); return config; }, (error) => { - console.error("API 요청 오류:", error); + console.error("❌ API 요청 오류:", error); return Promise.reject(error); }, ); @@ -73,21 +85,34 @@ apiClient.interceptors.request.use( // 응답 인터셉터 apiClient.interceptors.response.use( (response: AxiosResponse) => { - console.log("API 응답:", response.status, response.config.url, response.data); + console.log("✅ API 응답:", response.status, response.config.url, response.data); return response; }, (error: AxiosError) => { - console.error("API 응답 오류:", { + console.error("❌ API 응답 오류:", { status: error.response?.status, statusText: error.response?.statusText, url: error.config?.url, data: error.response?.data, message: error.message, + headers: error.config?.headers, }); + // 401 에러 시 상세 정보 출력 + if (error.response?.status === 401) { + console.error("🚨 401 Unauthorized 오류 상세 정보:", { + url: error.config?.url, + method: error.config?.method, + headers: error.config?.headers, + requestData: error.config?.data, + responseData: error.response?.data, + token: TokenManager.getToken() ? "존재" : "없음", + }); + } + // 401 에러 시 토큰 제거 및 로그인 페이지로 리다이렉트 if (error.response?.status === 401 && typeof window !== "undefined") { - console.log("401 에러 감지 - 토큰 제거 및 로그인 페이지로 리다이렉트"); + console.log("🔄 401 에러 감지 - 토큰 제거 및 로그인 페이지로 리다이렉트"); localStorage.removeItem("authToken"); // 로그인 페이지가 아닌 경우에만 리다이렉트 diff --git a/frontend/lib/api/user.ts b/frontend/lib/api/user.ts index a212d22c..8d98b86f 100644 --- a/frontend/lib/api/user.ts +++ b/frontend/lib/api/user.ts @@ -1,4 +1,4 @@ -import { API_BASE_URL } from "./client"; +import { apiClient } from "./client"; /** * 사용자 관리 API 클라이언트 @@ -15,89 +15,64 @@ interface ApiResponse { msg?: string; } -/** - * API 호출 공통 함수 - */ -async function apiCall(endpoint: string, options: RequestInit = {}): Promise> { - try { - const response = await fetch(`${API_BASE_URL}${endpoint}`, { - headers: { - "Content-Type": "application/json", - ...options.headers, - }, - credentials: "include", // 쿠키 포함 - ...options, - }); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const data = await response.json(); - return data; - } catch (error) { - console.error("API 호출 오류:", error); - throw error; - } -} - /** * 사용자 목록 조회 */ export async function getUserList(params?: Record) { - const searchParams = new URLSearchParams(); + try { + console.log("📡 사용자 목록 API 호출:", params); - // 모든 검색 파라미터를 동적으로 처리 - if (params) { - Object.entries(params).forEach(([key, value]) => { - if (value !== undefined && value !== null && value !== "") { - searchParams.append(key, String(value)); - } + const response = await apiClient.get("/admin/users", { + params: params, }); + + console.log("✅ 사용자 목록 API 응답:", response.data); + return response.data; + } catch (error) { + console.error("❌ 사용자 목록 API 오류:", error); + throw error; } - - const queryString = searchParams.toString(); - const endpoint = `/admin/users${queryString ? `?${queryString}` : ""}`; - - console.log("📡 최종 API 호출 URL:", endpoint); - const response = await apiCall(endpoint); - - // 전체 response 객체를 그대로 반환 (success, data, total 포함) - return response; } /** * 사용자 정보 단건 조회 */ export async function getUserInfo(userId: string) { - const response = await apiCall(`/admin/users/${userId}`); + try { + const response = await apiClient.get(`/admin/users/${userId}`); - if (response.success && response.data) { - return response.data; + if (response.data.success && response.data.data) { + return response.data.data; + } + + throw new Error(response.data.message || "사용자 정보 조회에 실패했습니다."); + } catch (error) { + console.error("❌ 사용자 정보 조회 오류:", error); + throw error; } - - throw new Error(response.message || "사용자 정보 조회에 실패했습니다."); } /** * 사용자 등록 */ export async function createUser(userData: any) { - const response = await apiCall("/admin/users", { - method: "POST", - body: JSON.stringify(userData), - }); + try { + const response = await apiClient.post("/admin/users", userData); - // 백엔드에서 result 필드를 사용하므로 이에 맞춰 처리 - if (response.result === true || response.success === true) { - return { - success: true, - message: response.msg || response.message || "사용자가 성공적으로 등록되었습니다.", - data: response, - }; + // 백엔드에서 result 필드를 사용하므로 이에 맞춰 처리 + if (response.data.result === true || response.data.success === true) { + return { + success: true, + message: response.data.msg || response.data.message || "사용자가 성공적으로 등록되었습니다.", + data: response.data, + }; + } + + throw new Error(response.data.msg || response.data.message || "사용자 등록에 실패했습니다."); + } catch (error) { + console.error("❌ 사용자 등록 오류:", error); + throw error; } - - throw new Error(response.msg || response.message || "사용자 등록에 실패했습니다."); } // 사용자 수정 기능 제거됨 @@ -106,12 +81,9 @@ export async function createUser(userData: any) { * 사용자 상태 변경 */ export async function updateUserStatus(userId: string, status: string) { - const response = await apiCall(`/admin/users/${userId}/status`, { - method: "PUT", - body: JSON.stringify({ status }), - }); + const response = await apiClient.put(`/admin/users/${userId}/status`, { status }); - return response; + return response.data; } // 사용자 삭제 기능 제거됨 @@ -137,34 +109,31 @@ export async function getUserHistory(userId: string, params?: Record { + const token = localStorage.getItem("authToken"); + console.log("🔍 토큰 상태 확인:", token ? "존재" : "없음"); + return !!token; + }, + + // 토큰 강제 동기화 (다른 탭에서 설정된 토큰을 현재 탭에 복사) + forceSync: () => { + const token = localStorage.getItem("authToken"); + if (token) { + // sessionStorage에도 복사 + sessionStorage.setItem("authToken", token); + console.log("🔄 토큰 강제 동기화 완료"); + return true; + } + return false; + }, + + // 토큰 복원 시도 (sessionStorage에서 복원) + restoreFromSession: () => { + const sessionToken = sessionStorage.getItem("authToken"); + if (sessionToken) { + localStorage.setItem("authToken", sessionToken); + console.log("🔄 sessionStorage에서 토큰 복원 완료"); + return true; + } + return false; + }, + + // 토큰 유효성 검증 + validateToken: (token: string) => { + if (!token) return false; + + try { + // JWT 토큰 구조 확인 (header.payload.signature) + const parts = token.split("."); + if (parts.length !== 3) return false; + + // payload 디코딩 시도 + const payload = JSON.parse(atob(parts[1])); + const now = Math.floor(Date.now() / 1000); + + // 만료 시간 확인 + if (payload.exp && payload.exp < now) { + console.log("❌ 토큰 만료됨"); + return false; + } + + console.log("✅ 토큰 유효성 검증 통과"); + return true; + } catch (error) { + console.log("❌ 토큰 유효성 검증 실패:", error); + return false; + } + }, +};