feat: 메뉴 폴링 및 문서 업데이트
- 메뉴 폴링 API 및 훅 추가 (useMenuPolling, menuRefresh) - AuthenticatedLayout 메뉴 새로고침 연동 - 품질검사 체크리스트 문서 추가 - Vercel 배포 가이드 추가 - 동적 메뉴 리프레시 계획 문서 추가 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
92
src/app/api/menus/route.ts
Normal file
92
src/app/api/menus/route.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
/**
|
||||
* 🔵 Next.js 내부 API - 메뉴 조회 프록시 (PHP 백엔드로 전달)
|
||||
*
|
||||
* ⚡ 설계 목적:
|
||||
* - 동적 메뉴 갱신: 재로그인 없이 메뉴 목록 갱신
|
||||
* - 보안: HttpOnly 쿠키에서 토큰을 읽어 백엔드로 전달
|
||||
*
|
||||
* 🔄 동작 흐름:
|
||||
* 1. 클라이언트 → Next.js /api/menus
|
||||
* 2. Next.js: HttpOnly 쿠키에서 access_token 읽기
|
||||
* 3. Next.js → PHP /api/v1/menus (메뉴 조회 요청)
|
||||
* 4. Next.js → 클라이언트 (메뉴 목록 응답)
|
||||
*
|
||||
* 📌 백엔드 API 요청 사항:
|
||||
* - 엔드포인트: GET /api/v1/menus
|
||||
* - 인증: Bearer 토큰 필요
|
||||
* - 응답: { menus: [...] } (로그인 응답의 menus와 동일 구조)
|
||||
*
|
||||
* @see claudedocs/architecture/[PLAN-2025-12-29] dynamic-menu-refresh.md
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// HttpOnly 쿠키에서 access_token 읽기
|
||||
const accessToken = request.cookies.get('access_token')?.value;
|
||||
|
||||
if (!accessToken) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Unauthorized', message: '인증 토큰이 없습니다' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// PHP 백엔드 메뉴 API 호출
|
||||
const backendUrl = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/menus`;
|
||||
|
||||
const response = await fetch(backendUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
// 백엔드 에러 응답 전달
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
console.error('[Menu API] Backend error:', response.status, errorData);
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Backend Error',
|
||||
message: errorData.message || '메뉴 조회에 실패했습니다',
|
||||
status: response.status
|
||||
},
|
||||
{ status: response.status }
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// 백엔드 응답 구조: { data: [...] } (ApiResponse::handle 표준)
|
||||
// 또는 로그인 응답과 동일한 { menus: [...] } 형태일 수 있음
|
||||
const menus = data.data || data.menus || (Array.isArray(data) ? data : null);
|
||||
|
||||
// 메뉴 데이터 검증
|
||||
if (!menus || !Array.isArray(menus)) {
|
||||
console.error('[Menu API] Invalid response format:', data);
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid Response', message: '메뉴 데이터 형식이 올바르지 않습니다' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
// 응답 구조 통일: { menus: [...] }
|
||||
return NextResponse.json({ menus }, { status: 200 });
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Menu API] Proxy error:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Internal Server Error',
|
||||
message: error instanceof Error ? error.message : '서버 오류가 발생했습니다'
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user