diff --git a/src/components/quality/PerformanceReportManagement/PerformanceReportList.tsx b/src/components/quality/PerformanceReportManagement/PerformanceReportList.tsx index 4db613dc..dafb21a7 100644 --- a/src/components/quality/PerformanceReportManagement/PerformanceReportList.tsx +++ b/src/components/quality/PerformanceReportManagement/PerformanceReportList.tsx @@ -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( diff --git a/src/components/quality/PerformanceReportManagement/actions.ts b/src/components/quality/PerformanceReportManagement/actions.ts index 602fb1f3..a7dd0564 100644 --- a/src/components/quality/PerformanceReportManagement/actions.ts +++ b/src/components/quality/PerformanceReportManagement/actions.ts @@ -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: '서버 오류가 발생했습니다.' }; + } +}