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:
byeongcheolryu
2025-12-29 14:54:27 +09:00
parent fb2be8651e
commit 69832b4c58
9 changed files with 1060 additions and 1 deletions

View 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 }
);
}
}