feat(WEB): UniversalListPage 전체 마이그레이션 및 코드 정리
- UniversalListPage/IntegratedListTemplateV2 컴포넌트 기능 개선 - 회계, HR, 건설, 고객센터, 결재, 설정 등 전체 리스트 컴포넌트 마이그레이션 - 테스트 페이지 및 미사용 API 라우트 정리 (board-test, order-management-test 등) - 미들웨어 토큰 갱신 로직 개선 - AuthenticatedLayout 구조 개선 - claudedocs 문서 업데이트 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,65 +0,0 @@
|
||||
/**
|
||||
* 파일 다운로드 프록시 API
|
||||
*
|
||||
* 백엔드 파일 다운로드 API는 인증이 필요하므로,
|
||||
* Next.js API 라우트를 통해 인증된 요청을 프록시합니다.
|
||||
*
|
||||
* GET /api/files/[id]/download
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const token = request.cookies.get('access_token')?.value;
|
||||
|
||||
if (!token) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: '인증이 필요합니다.' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const backendUrl = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/files/${id}/download`;
|
||||
|
||||
const response = await fetch(backendUrl, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: '파일을 찾을 수 없습니다.' },
|
||||
{ status: response.status }
|
||||
);
|
||||
}
|
||||
|
||||
// 파일 데이터와 헤더 전달
|
||||
const blob = await response.blob();
|
||||
const contentType = response.headers.get('content-type') || 'application/octet-stream';
|
||||
const contentDisposition = response.headers.get('content-disposition');
|
||||
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': contentType,
|
||||
'Cache-Control': 'public, max-age=31536000', // 1년 캐시
|
||||
};
|
||||
|
||||
if (contentDisposition) {
|
||||
headers['Content-Disposition'] = contentDisposition;
|
||||
}
|
||||
|
||||
return new NextResponse(blob, { headers });
|
||||
} catch (error) {
|
||||
console.error('[FileDownload] Error:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, message: '파일 다운로드 중 오류가 발생했습니다.' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import { NextRequest } from 'next/server';
|
||||
import { proxyToPhpBackend } from '@/lib/api/php-proxy';
|
||||
|
||||
/**
|
||||
* 특정 페이지 조회 API
|
||||
*
|
||||
* 엔드포인트: GET /api/tenants/{tenantId}/item-master-config/pages/{pageId}
|
||||
*/
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ tenantId: string; pageId: string }> }
|
||||
) {
|
||||
const { tenantId, pageId } = await params;
|
||||
|
||||
return proxyToPhpBackend(
|
||||
request,
|
||||
`/api/v1/tenants/${tenantId}/item-master-config/pages/${pageId}`,
|
||||
{ method: 'GET' }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 페이지 업데이트 API
|
||||
*
|
||||
* 엔드포인트: PUT /api/tenants/{tenantId}/item-master-config/pages/{pageId}
|
||||
*/
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ tenantId: string; pageId: string }> }
|
||||
) {
|
||||
const { tenantId, pageId } = await params;
|
||||
const body = await request.json();
|
||||
|
||||
return proxyToPhpBackend(
|
||||
request,
|
||||
`/api/v1/tenants/${tenantId}/item-master-config/pages/${pageId}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(body),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 페이지 삭제 API
|
||||
*
|
||||
* 엔드포인트: DELETE /api/tenants/{tenantId}/item-master-config/pages/{pageId}
|
||||
*/
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ tenantId: string; pageId: string }> }
|
||||
) {
|
||||
const { tenantId, pageId } = await params;
|
||||
|
||||
return proxyToPhpBackend(
|
||||
request,
|
||||
`/api/v1/tenants/${tenantId}/item-master-config/pages/${pageId}`,
|
||||
{ method: 'DELETE' }
|
||||
);
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
import { NextRequest } from 'next/server';
|
||||
import { proxyToPhpBackend, appendQueryParams } from '@/lib/api/php-proxy';
|
||||
|
||||
/**
|
||||
* 품목기준관리 전체 설정 조회 API
|
||||
*
|
||||
* 엔드포인트: GET /api/tenants/{tenantId}/item-master-config
|
||||
*
|
||||
* 역할:
|
||||
* - PHP 백엔드로 단순 프록시
|
||||
* - tenant.id 검증은 PHP에서 수행
|
||||
* - PHP가 403 반환하면 그대로 전달
|
||||
*/
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ tenantId: string }> }
|
||||
) {
|
||||
const { tenantId } = await params;
|
||||
const { searchParams } = new URL(request.url);
|
||||
|
||||
// PHP 엔드포인트 생성 (query params 포함)
|
||||
const phpEndpoint = appendQueryParams(
|
||||
`/api/v1/tenants/${tenantId}/item-master-config`,
|
||||
searchParams
|
||||
);
|
||||
|
||||
return proxyToPhpBackend(request, phpEndpoint, {
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 품목기준관리 전체 설정 저장 API
|
||||
*
|
||||
* 엔드포인트: POST /api/tenants/{tenantId}/item-master-config
|
||||
*/
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ tenantId: string }> }
|
||||
) {
|
||||
const { tenantId } = await params;
|
||||
const body = await request.json();
|
||||
|
||||
return proxyToPhpBackend(
|
||||
request,
|
||||
`/api/v1/tenants/${tenantId}/item-master-config`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 품목기준관리 전체 설정 업데이트 API
|
||||
*
|
||||
* 엔드포인트: PUT /api/tenants/{tenantId}/item-master-config
|
||||
*/
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ tenantId: string }> }
|
||||
) {
|
||||
const { tenantId } = await params;
|
||||
const body = await request.json();
|
||||
|
||||
return proxyToPhpBackend(
|
||||
request,
|
||||
`/api/v1/tenants/${tenantId}/item-master-config`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(body),
|
||||
}
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user