From 1f963a5c605268acf5cb60389bd9536c09f31bb5 Mon Sep 17 00:00:00 2001 From: kent Date: Sat, 20 Dec 2025 13:43:37 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20MNG=20=E2=86=92=20DEV=20=EC=9E=90?= =?UTF-8?q?=EB=8F=99=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B0=9C=EB=B0=9C=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - front/AUTO_LOGIN_GUIDE.md: React 프론트엔드 구현 가이드 - projects/auto-login/PROGRESS.md: 개발 진행 상태 및 세션 재개 가이드 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- front/AUTO_LOGIN_GUIDE.md | 353 ++++++++++++++++++++++++++++++++ projects/auto-login/PROGRESS.md | 187 +++++++++++++++++ 2 files changed, 540 insertions(+) create mode 100644 front/AUTO_LOGIN_GUIDE.md create mode 100644 projects/auto-login/PROGRESS.md diff --git a/front/AUTO_LOGIN_GUIDE.md b/front/AUTO_LOGIN_GUIDE.md new file mode 100644 index 0000000..14d8034 --- /dev/null +++ b/front/AUTO_LOGIN_GUIDE.md @@ -0,0 +1,353 @@ +# 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://mng.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 \ No newline at end of file diff --git a/projects/auto-login/PROGRESS.md b/projects/auto-login/PROGRESS.md new file mode 100644 index 0000000..0a527f4 --- /dev/null +++ b/projects/auto-login/PROGRESS.md @@ -0,0 +1,187 @@ +# MNG → DEV 자동 로그인 기능 개발 + +> **최종 업데이트:** 2025-12-20 +> **상태:** ✅ API/MNG 백엔드 완료, 🔄 React 프론트엔드 대기 + +--- + +## 📋 필수 참조 문서 + +**⚠️ 작업 시작 전 반드시 확인:** +- [x] `docs/guides/PROJECT_DEVELOPMENT_POLICY.md` - 개발 공통 정책 +- [x] `docs/front/AUTO_LOGIN_GUIDE.md` - React 프론트엔드 구현 가이드 + +--- + +## 🎯 개요 + +MNG 관리자가 사용자 목록에서 "DEV 접속" 버튼을 클릭하면, 해당 사용자로 자동 로그인되어 DEV 사이트로 이동하는 기능. + +### 환경별 URL +| 환경 | MNG | DEV | +|------|-----|-----| +| 로컬 | https://mng.sam.kr | https://dev.sam.kr | +| 개발 | https://mng.codebridge-x.com | https://dev.codebridge-x.com | + +### 아키텍처 +``` +MNG에서 DEV 접속 클릭 + ↓ +MNG API → login_tokens 테이블에 One-Time Token 생성 (5분 만료) + ↓ +MNG → DEV URL + token 파라미터로 새 창 열기 + ↓ +DEV React → API token-login 호출 + ↓ +API → token 검증 → access_token 발급 → token 삭제 (1회용) + ↓ +DEV React → 로그인 완료 → 대시보드 이동 +``` + +--- + +## 📊 진행 상태 + +### Phase 1: API 백엔드 (api 프로젝트) ✅ 완료 + +| 단계 | 작업 | 상태 | 비고 | +|------|------|------|------| +| 1.1 | login_tokens 마이그레이션 생성 | ✅ 완료 | 2025_12_20_132721_create_login_tokens_table.php | +| 1.2 | LoginToken 모델 생성 | ✅ 완료 | app/Models/LoginToken.php | +| 1.3 | token-login 엔드포인트 추가 | ✅ 완료 | ApiController.php | +| 1.4 | 라우트 추가 | ✅ 완료 | POST /api/v1/token-login | + +### Phase 2: MNG 백엔드 (mng 프로젝트) ✅ 완료 + +| 단계 | 작업 | 상태 | 비고 | +|------|------|------|------| +| 2.1 | DEV_APP_URL 환경설정 | ✅ 완료 | .env, config/services.php | +| 2.2 | LoginToken 모델 생성 | ✅ 완료 | app/Models/LoginToken.php | +| 2.3 | loginToken API 메서드 추가 | ✅ 완료 | Api/Admin/UserController.php | +| 2.4 | 라우트 추가 | ✅ 완료 | POST /api/admin/users/{id}/login-token | +| 2.5 | "DEV 접속" 버튼 UI 추가 | ✅ 완료 | users/partials/table.blade.php | +| 2.6 | JavaScript 함수 추가 | ✅ 완료 | users/index.blade.php | + +### Phase 3: React 프론트엔드 (react 프로젝트) 🔄 대기 + +| 단계 | 작업 | 상태 | 비고 | +|------|------|------|------| +| 3.1 | /auto-login 페이지 생성 | ⏳ 대기 | 프론트엔드 담당 | +| 3.2 | token-login API Route 생성 | ⏳ 대기 | 프론트엔드 담당 | +| 3.3 | auth-config.ts 수정 | ⏳ 대기 | 프론트엔드 담당 | + +> **Note:** React 구현은 `docs/front/AUTO_LOGIN_GUIDE.md` 참조 + +--- + +## 📁 변경된 파일 + +### API 프로젝트 (api/) +``` +api/ +├── database/migrations/ +│ └── 2025_12_20_132721_create_login_tokens_table.php (생성) +├── app/Models/ +│ └── LoginToken.php (생성) +├── app/Http/Controllers/Api/V1/ +│ └── ApiController.php (수정 - tokenLogin 메서드 추가) +└── routes/ + └── api.php (수정 - token-login 라우트 추가) +``` + +### MNG 프로젝트 (mng/) +``` +mng/ +├── .env (수정 - DEV_APP_URL 추가) +├── config/ +│ └── services.php (수정 - dev 설정 추가) +├── app/Models/ +│ └── LoginToken.php (생성) +├── app/Http/Controllers/Api/Admin/ +│ └── UserController.php (수정 - loginToken 메서드 추가) +├── routes/ +│ └── api.php (수정 - login-token 라우트 추가) +└── resources/views/users/ + ├── partials/table.blade.php (수정 - DEV 접속 버튼 추가) + └── index.blade.php (수정 - openDevSite 함수 추가) +``` + +### React 프로젝트 (react/) - 프론트엔드 담당 +``` +react/ +├── src/app/[locale]/auto-login/page.tsx (생성 예정) +├── src/app/api/auth/token-login/route.ts (생성 예정) +└── src/lib/api/auth/auth-config.ts (수정 예정) +``` + +--- + +## 🗄️ 테이블 설계 + +### login_tokens 테이블 + +```php +Schema::create('login_tokens', function (Blueprint $table) { + $table->id(); + $table->unsignedBigInteger('user_id')->comment('사용자 ID'); + $table->string('token', 64)->unique()->comment('One-Time Token (64자)'); + $table->timestamp('expires_at')->comment('만료 시간'); + $table->timestamps(); + + $table->index('token'); + $table->index('expires_at'); + $table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete(); +}); +``` + +**보안 고려사항:** +- 토큰: 64자 랜덤 문자열 +- 만료: 5분 +- 1회용: 사용 후 즉시 삭제 +- HTTPS 필수 + +--- + +## 📝 변경 이력 + +| 날짜 | 시간 | 작업 내용 | 상태 | +|------|------|----------|------| +| 2025-12-20 | - | React 구현 가이드 문서화 | ✅ | +| 2025-12-20 | - | 개발 계획 문서 생성 | ✅ | +| 2025-12-20 | - | API 백엔드 개발 완료 | ✅ | +| 2025-12-20 | - | MNG 백엔드 개발 완료 | ✅ | +| 2025-12-20 | - | React 프론트엔드 대기 | 🔄 | + +--- + +## 🔄 세션 재개 시 체크리스트 + +세션이 끊겼다가 다시 시작할 때: + +1. [x] 이 문서 (PROGRESS.md) 확인 +2. [x] `docs/guides/PROJECT_DEVELOPMENT_POLICY.md` 정책 확인 +3. [x] 현재 단계 및 상태 파악 +4. [x] 다음 작업 항목 확인 +5. [ ] 관련 파일 상태 확인 (git status) +6. [ ] 이어서 작업 진행 + +--- + +## 🚀 다음 단계 + +1. **프론트엔드 개발자 검토** + - `docs/front/AUTO_LOGIN_GUIDE.md` 문서 검토 + - React 구현 진행 + +2. **테스트** + - MNG 사용자 목록에서 "DEV 접속" 버튼 클릭 + - DEV 사이트 자동 로그인 확인 + - 대시보드 이동 확인 + +3. **커밋** + - API 프로젝트 커밋 + - MNG 프로젝트 커밋 + +--- + +> **Note:** 이 문서는 작업 진행에 따라 실시간 업데이트됩니다.