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:
byeongcheolryu
2026-01-16 15:19:09 +09:00
parent 8639eee5df
commit ad493bcea6
90 changed files with 19864 additions and 20305 deletions

View File

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

View File

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

View File

@@ -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' }
);
}

View File

@@ -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),
}
);
}