# MNG → DEV 자동 로그인 기능 (React 구현 가이드) ## 개요 MNG 관리자가 사용자 목록에서 "DEV 접속" 버튼을 클릭하면, 해당 사용자로 자동 로그인되어 DEV 사이트로 이동하는 기능. ## 아키텍처 ``` ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ MNG │ │ API │ │ React │ │ (관리자) │ │ (토큰검증) │ │ (프론트) │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ │ │ 1. DEV접속 클릭 │ │ │ │ │ 2. 토큰생성 ─────────────┼─── DB 저장 ───────┤ │ │ (login_tokens) │ 3. URL반환 │ │ │ │ │ 4. 새창열기 ─────────────┼───────────────────┼─→ /auto-login?token=xxx │ │ │ │ 5. 토큰검증 ←────────────┤ │ │ │ │ 6. access_token 발급 ────┤ │ │ │ │ 7. 토큰삭제 (1회용) │ │ │ │ │ │ 8. 로그인 완료 → 대시보드 ``` ## 환경별 URL | 환경 | MNG | DEV | |------|-----|-----| | 로컬 | https://mng.sam.kr | https://dev.sam.kr | | 개발 | https://admin.codebridge-x.com | https://dev.codebridge-x.com | --- ## React 구현 작업 ### 1. 신규 파일: `/auto-login` 페이지 **경로:** `src/app/[locale]/auto-login/page.tsx` ```tsx "use client"; import { useEffect, useState } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import { transformApiMenusToMenuItems } from "@/lib/utils/menuTransform"; export default function AutoLoginPage() { const router = useRouter(); const searchParams = useSearchParams(); const [status, setStatus] = useState<"loading" | "success" | "error">("loading"); const [message, setMessage] = useState("자동 로그인 처리 중..."); useEffect(() => { const token = searchParams.get("token"); if (!token) { setStatus("error"); setMessage("유효하지 않은 접근입니다."); return; } const performAutoLogin = async () => { try { // 1. 기존 세션 클리어 (기존 로그인 사용자 로그아웃) localStorage.removeItem("user"); // 기존 쿠키도 클리어하기 위해 logout API 호출 (선택사항) try { await fetch("/api/auth/logout", { method: "POST" }); } catch { // 로그아웃 실패해도 계속 진행 } // 2. 토큰 로그인 API 호출 const response = await fetch("/api/auth/token-login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ token }), }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || "토큰이 만료되었거나 유효하지 않습니다."); } const data = await response.json(); // 3. 사용자 정보 저장 (기존 로그인과 동일한 형식) const transformedMenus = transformApiMenusToMenuItems(data.menus || []); const userData = { name: data.user?.name, position: data.roles?.[0]?.description || "사용자", userId: data.user?.user_id, menu: transformedMenus, roles: data.roles || [], tenant: data.tenant || {}, }; localStorage.setItem("user", JSON.stringify(userData)); setStatus("success"); setMessage("로그인 성공! 대시보드로 이동합니다..."); // 4. 대시보드로 리다이렉트 setTimeout(() => router.push("/dashboard"), 500); } catch (error) { console.error("Auto login failed:", error); setStatus("error"); setMessage(error instanceof Error ? error.message : "로그인에 실패했습니다."); } }; performAutoLogin(); }, [searchParams, router]); return (
{status === "loading" && (
)} {status === "success" && (
)} {status === "error" && (
)}

{message}

{status === "error" && ( )}
); } ``` --- ### 2. 신규 파일: 토큰 로그인 API Route **경로:** `src/app/api/auth/token-login/route.ts` ```ts import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; /** * 토큰 자동 로그인 API * * MNG 관리자가 생성한 One-Time Token으로 자동 로그인 처리 * * 흐름: * 1. token 파라미터 검증 * 2. PHP API /v1/auth/token-login 호출 * 3. access_token을 HttpOnly 쿠키로 저장 * 4. 사용자 정보 반환 */ export async function POST(request: NextRequest) { try { const { token } = await request.json(); if (!token) { return NextResponse.json( { error: "Token is required" }, { status: 400 } ); } // PHP 백엔드 호출 const backendResponse = await fetch( `${process.env.NEXT_PUBLIC_API_URL}/api/v1/auth/token-login`, { method: "POST", headers: { "Content-Type": "application/json", "Accept": "application/json", "X-API-KEY": process.env.NEXT_PUBLIC_API_KEY || "", }, body: JSON.stringify({ token }), } ); if (!backendResponse.ok) { const errorText = await backendResponse.text(); console.error("Token login backend error:", errorText); return NextResponse.json( { error: "Invalid or expired token" }, { status: 401 } ); } const data = await backendResponse.json(); // HttpOnly 쿠키 설정 (기존 login/route.ts와 동일) const isProduction = process.env.NODE_ENV === "production"; const accessTokenCookie = [ `access_token=${data.access_token}`, "HttpOnly", ...(isProduction ? ["Secure"] : []), "SameSite=Lax", "Path=/", `Max-Age=${data.expires_in || 7200}`, ].join("; "); const refreshTokenCookie = [ `refresh_token=${data.refresh_token}`, "HttpOnly", ...(isProduction ? ["Secure"] : []), "SameSite=Lax", "Path=/", "Max-Age=604800", ].join("; "); console.log("✅ Token login successful - tokens stored in HttpOnly cookies"); const response = NextResponse.json({ message: data.message, user: data.user, tenant: data.tenant, menus: data.menus, roles: data.roles, expires_in: data.expires_in, expires_at: data.expires_at, }); response.headers.append("Set-Cookie", accessTokenCookie); response.headers.append("Set-Cookie", refreshTokenCookie); return response; } catch (error) { console.error("Token login error:", error); return NextResponse.json( { error: "Internal server error" }, { status: 500 } ); } } ``` --- ### 3. 수정 파일: auth-config.ts **경로:** `src/lib/api/auth/auth-config.ts` ```ts // guestOnlyRoutes 배열에 /auto-login 추가 guestOnlyRoutes: [ '/login', '/forgot-password', '/auto-login', // 추가 ], ``` --- ### 4. 수정 파일: middleware.ts (확인 필요) `/auto-login` 경로가 인증 없이 접근 가능한지 확인 필요. `guestOnlyRoutes`에 추가되면 자동으로 처리될 수 있음. --- ## API 엔드포인트 (백엔드) ### POST /api/v1/auth/token-login **Request:** ```json { "token": "abc123..." // 64자 랜덤 문자열 } ``` **Response (성공):** ```json { "message": "로그인 성공", "access_token": "1|xxx...", "refresh_token": "2|yyy...", "token_type": "Bearer", "expires_in": 7200, "expires_at": "2025-12-20 15:00:00", "user": { "id": 1, "user_id": "testuser", "name": "테스트 사용자", "email": "test@example.com" }, "tenant": { ... }, "menus": [ ... ], "roles": [ ... ] } ``` **Response (실패):** ```json { "error": "Invalid or expired token" } ``` --- ## 보안 고려사항 1. **토큰 1회 사용**: 사용 후 즉시 삭제 2. **토큰 만료**: 5분 (생성 후) 3. **HTTPS 필수**: 토큰이 URL에 노출되므로 4. **관리자 권한**: MNG 로그인한 관리자만 토큰 생성 가능 --- ## 테스트 시나리오 1. MNG에서 사용자 목록 → "DEV 접속" 버튼 클릭 2. 새 창에서 DEV 사이트 열림 3. 자동 로그인 처리 (로딩 표시) 4. 성공 시 대시보드로 이동 5. 실패 시 에러 메시지 + 로그인 페이지 이동 버튼 --- ## 작성일 2025-12-20