feat: [quality] 실적신고 확정건 엑셀 다운로드 프론트엔드 연동

- exportConfirmedExcel() 서버 액션 추가 (Blob 다운로드 패턴)
- handleExcelDownload 함수를 실제 API 호출로 변경
- 연도/분기 필터 파라미터 전달
This commit is contained in:
김보곤
2026-03-17 15:57:08 +09:00
parent b33f7d9b11
commit 06233387b0
2 changed files with 71 additions and 3 deletions

View File

@@ -48,6 +48,7 @@ import {
unconfirmReports,
distributeReports,
updateMemo,
exportConfirmedExcel,
} from './actions';
import { confirmStatusColorMap } from './mockData';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
@@ -209,9 +210,28 @@ export function PerformanceReportList() {
}
}, [memoSelectedIds]);
const handleExcelDownload = useCallback(() => {
toast.info('확정건 엑셀 다운로드 기능은 API 연동 후 활성화됩니다.');
}, []);
const handleExcelDownload = useCallback(async () => {
const quarterNum = quarter === '전체' ? Math.ceil(new Date().getMonth() / 3) || 1 : Number(quarter);
try {
const result = await exportConfirmedExcel({ year, quarter: quarterNum });
if (result.success && result.data) {
const url = URL.createObjectURL(result.data);
const a = document.createElement('a');
a.href = url;
a.download = result.filename || `판매실적대장_${year}년_${quarterNum}분기.xlsx`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
toast.success('엑셀 다운로드 완료');
} else {
toast.error(result.error || '엑셀 다운로드에 실패했습니다.');
}
} catch (error) {
if (isNextRedirectError(error)) throw error;
toast.error('엑셀 다운로드에 실패했습니다.');
}
}, [year, quarter]);
// ===== 통계 카드 =====
const stats: StatCard[] = useMemo(

View File

@@ -7,6 +7,7 @@
* - GET /api/v1/quality/performance-reports - 분기별 실적신고 목록
* - GET /api/v1/quality/performance-reports/stats - 통계
* - GET /api/v1/quality/performance-reports/missing - 누락체크 목록
* - GET /api/v1/quality/performance-reports/export-excel - 확정건 엑셀 다운로드
* - PATCH /api/v1/quality/performance-reports/confirm - 선택 확정
* - PATCH /api/v1/quality/performance-reports/unconfirm - 확정 해제
* - PATCH /api/v1/quality/performance-reports/memo - 메모 일괄 적용
@@ -14,6 +15,7 @@
import { executeServerAction } from '@/lib/api/execute-server-action';
import { buildApiUrl } from '@/lib/api/query-params';
import { cookies } from 'next/headers';
import type {
PerformanceReport,
PerformanceReportStats,
@@ -301,3 +303,49 @@ export async function updateMemo(ids: string[], memo: string): Promise<{
if (!result.success && USE_MOCK_FALLBACK) return { success: true };
return { success: result.success, error: result.error, __authError: result.__authError };
}
// ===== 확정건 엑셀 다운로드 =====
export async function exportConfirmedExcel(params: {
year: number;
quarter: number;
}): Promise<{
success: boolean;
data?: Blob;
filename?: string;
error?: string;
}> {
try {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
const headers: HeadersInit = {
'Accept': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'Authorization': token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
const url = buildApiUrl('/api/v1/quality/performance-reports/export-excel', {
year: params.year,
quarter: params.quarter,
});
const response = await fetch(url, { method: 'GET', headers });
if (!response.ok) {
return { success: false, error: `API 오류: ${response.status}` };
}
const blob = await response.blob();
const contentDisposition = response.headers.get('Content-Disposition');
const filenameMatch = contentDisposition?.match(/filename\*?=(?:UTF-8'')?(.+)/);
const filename = filenameMatch?.[1]
? decodeURIComponent(filenameMatch[1])
: `판매실적대장_${params.year}년_${params.quarter}분기.xlsx`;
return { success: true, data: blob, filename };
} catch (error) {
if (error instanceof Error && error.message?.includes('NEXT_REDIRECT')) throw error;
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}