From 06233387b0162e04ae456b23809c65bdee1949c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Tue, 17 Mar 2026 15:57:08 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20[quality]=20=EC=8B=A4=EC=A0=81=EC=8B=A0?= =?UTF-8?q?=EA=B3=A0=20=ED=99=95=EC=A0=95=EA=B1=B4=20=EC=97=91=EC=85=80=20?= =?UTF-8?q?=EB=8B=A4=EC=9A=B4=EB=A1=9C=EB=93=9C=20=ED=94=84=EB=A1=A0?= =?UTF-8?q?=ED=8A=B8=EC=97=94=EB=93=9C=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - exportConfirmedExcel() 서버 액션 추가 (Blob 다운로드 패턴) - handleExcelDownload 함수를 실제 API 호출로 변경 - 연도/분기 필터 파라미터 전달 --- .../PerformanceReportList.tsx | 26 ++++++++-- .../PerformanceReportManagement/actions.ts | 48 +++++++++++++++++++ 2 files changed, 71 insertions(+), 3 deletions(-) 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: '서버 오류가 발생했습니다.' }; + } +}