# 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 (
    <div className="min-h-screen bg-background flex items-center justify-center">
      <div className="text-center p-8">
        {status === "loading" && (
          <div className="animate-spin h-12 w-12 border-4 border-primary border-t-transparent rounded-full mx-auto mb-4" />
        )}
        {status === "success" && (
          <div className="h-12 w-12 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
            <svg className="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
            </svg>
          </div>
        )}
        {status === "error" && (
          <div className="h-12 w-12 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
            <svg className="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
            </svg>
          </div>
        )}
        <p className={`text-lg ${status === "error" ? "text-destructive" : "text-muted-foreground"}`}>
          {message}
        </p>
        {status === "error" && (
          <button
            onClick={() => router.push("/login")}
            className="mt-4 px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90"
          >
            로그인 페이지로 이동
          </button>
        )}
      </div>
    </div>
  );
}
```

---

### 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