From dad90af83db465388c2cd82ac94ef9ffe4227119 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Wed, 18 Mar 2026 14:41:03 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20[changes]=20=ED=94=84=EB=A1=A0=ED=8A=B8?= =?UTF-8?q?=EC=97=94=EB=93=9C=20=EA=B0=80=EC=9D=B4=EB=93=9C=20=ED=94=BC?= =?UTF-8?q?=EB=93=9C=EB=B0=B1=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 파일 경로에서 react/ 접두사 제거 - SubscriptionManagement.tsx 수정 대상 추가 - buildApiUrl() 패턴 적용 - 'use server' 제약 반영 (브라우저 API 사용 불가) - HttpOnly 쿠키 + API proxy 인증 패턴 반영 - toast 메시지 변경 대상 명시 --- ...0318_subscription_export_frontend_guide.md | 119 ++++++++++-------- 1 file changed, 65 insertions(+), 54 deletions(-) diff --git a/changes/20260318_subscription_export_frontend_guide.md b/changes/20260318_subscription_export_frontend_guide.md index 304dc3e..2b11b46 100644 --- a/changes/20260318_subscription_export_frontend_guide.md +++ b/changes/20260318_subscription_export_frontend_guide.md @@ -27,9 +27,11 @@ ### 2.2 신규 엔드포인트 -| Method | Path | 설명 | -|--------|------|------| -| `GET` | `/api/v1/subscriptions/export/{id}/download` | 생성된 Excel 파일 다운로드 | +| Method | Path | 인증 | 설명 | +|--------|------|------|------| +| `GET` | `/api/v1/subscriptions/export/{id}/download` | API Key 필수 | 생성된 Excel 파일 다운로드 (바이너리 응답) | + +> **인증**: 다운로드 엔드포인트는 `ApiKeyMiddleware` 내부에 있어 API Key가 필요하다. 브라우저에서 직접 URL 접근 불가 — **Next.js API proxy 경유 필수**. ### 2.3 POST /export 응답 비교 @@ -70,14 +72,24 @@ --- -## 3. 프론트엔드 수정 가이드 +## 3. 수정 대상 파일 -### 3.1 현재 코드 (수정 대상) +| 파일 | 설명 | 수정 내용 | +|------|------|----------| +| `src/components/settings/SubscriptionManagement/actions.ts` | 서버 액션 | `requestDataExport` 응답 처리, 다운로드 서버 액션 추가 | +| `src/components/settings/SubscriptionManagement/SubscriptionManagement.tsx` | 메인 컴포넌트 | `handleExportData` 로직 + toast 메시지 변경 | +| `src/components/settings/SubscriptionManagement/SubscriptionClient.tsx` | 클라이언트 컴포넌트 | `handleExportData` 로직 + toast 메시지 변경 (동일 패턴) | -**파일:** `react/src/components/settings/SubscriptionManagement/actions.ts` +> **주의**: `SubscriptionManagement.tsx`와 `SubscriptionClient.tsx` 양쪽 모두에 동일한 `handleExportData` 로직과 `'완료되면 알림을 보내드립니다'` toast 메시지가 있다. **두 파일 모두 수정 필요**. +--- + +## 4. 프론트엔드 수정 가이드 + +### 4.1 현재 코드 + +**actions.ts:** ```typescript -// 현재: POST 후 toast만 표시 export async function requestDataExport( exportType: string = 'all' ): Promise> { @@ -91,10 +103,8 @@ export async function requestDataExport( } ``` -**파일:** `react/src/components/settings/SubscriptionManagement/SubscriptionClient.tsx` - +**SubscriptionManagement.tsx / SubscriptionClient.tsx (동일):** ```typescript -// 현재: "완료되면 알림을 보내드립니다" (실제로 알림 없음) const handleExportData = useCallback(async () => { setIsExporting(true); try { @@ -112,19 +122,21 @@ const handleExportData = useCallback(async () => { }, []); ``` -### 3.2 수정 방향 - -POST 응답에서 `id`를 받아 **즉시 다운로드 URL로 리다이렉트**하는 방식으로 변경한다. +### 4.2 수정 방향 #### actions.ts 수정 +`'use server'` 파일이므로 브라우저 API(`window`, `document`) 사용 불가. 서버 액션에서 다운로드 데이터를 blob으로 가져와 반환한다. + ```typescript +import { buildApiUrl } from '@/lib/api/query-params'; + // ===== 데이터 내보내기 요청 ===== export async function requestDataExport( exportType: string = 'all' ): Promise> { return executeServerAction({ - url: `${API_URL}/api/v1/subscriptions/export`, + url: buildApiUrl('/api/v1/subscriptions/export'), method: 'POST', body: { export_type: exportType }, transform: (data: { id: number; status: string; file_name: string | null }) => ({ @@ -136,13 +148,26 @@ export async function requestDataExport( }); } -// ===== 내보내기 파일 다운로드 URL 생성 ===== -export function getExportDownloadUrl(exportId: number): string { - return `${API_URL}/api/v1/subscriptions/export/${exportId}/download`; +// ===== 내보내기 파일 다운로드 (서버 액션) ===== +export async function downloadExportFile( + exportId: number +): Promise> { + return executeServerAction({ + url: buildApiUrl(`/api/v1/subscriptions/export/${exportId}/download`), + // 바이너리 응답 처리 — 기존 프로젝트 파일 다운로드 패턴에 맞게 구현 + // HttpOnly 쿠키가 서버 액션에서 자동 전달됨 + errorMessage: '파일 다운로드에 실패했습니다.', + }); } ``` -#### SubscriptionClient.tsx 수정 +> **대안: Next.js API proxy 경로 추가** +> +> 서버 액션 대신 `/api/proxy/subscriptions/export/{id}/download` 프록시 라우트를 추가하면 `window.open(proxyUrl)` 방식으로 직접 다운로드 가능. 기존 프로젝트에서 파일 다운로드를 어떤 방식으로 처리하는지에 따라 선택한다. + +#### SubscriptionManagement.tsx / SubscriptionClient.tsx 수정 + +두 파일 모두 동일하게 수정한다. ```typescript const handleExportData = useCallback(async () => { @@ -151,9 +176,9 @@ const handleExportData = useCallback(async () => { const result = await requestDataExport('all'); if (result.success && result.data) { if (result.data.status === 'completed') { - // 즉시 다운로드 - const downloadUrl = getExportDownloadUrl(result.data.id); - window.open(downloadUrl, '_blank'); + // 다운로드 처리 — 프로젝트 패턴에 맞게 구현 + // 방법 A) 서버 액션으로 blob 가져오기 + // 방법 B) Next.js API proxy URL로 window.open toast.success('Excel 파일이 다운로드됩니다.'); } else if (result.data.status === 'failed') { toast.error('내보내기 처리 중 오류가 발생했습니다.'); @@ -169,40 +194,23 @@ const handleExportData = useCallback(async () => { }, []); ``` -> **참고**: 다운로드 URL은 인증이 필요한 API이므로, `window.open` 대신 `fetch` + `blob` 패턴이 필요할 수 있다. 기존 프로젝트의 파일 다운로드 패턴을 확인하여 적용한다. +**변경 포인트 요약:** +- `requestDataExport` 응답에서 `status` 분기 처리 추가 +- toast 메시지: `'완료되면 알림을 보내드립니다'` → `'Excel 파일이 다운로드됩니다.'` +- 다운로드 처리 로직 추가 (프로젝트 패턴에 따라 A 또는 B 방식 선택) -### 3.3 인증이 필요한 경우 (fetch + blob 패턴) +### 4.3 다운로드 구현 방식 선택 -API가 `auth:sanctum` 미들웨어 아래에 있으므로, 토큰 전달이 필요하다면: +| 방식 | 구현 위치 | 장점 | 단점 | +|------|----------|------|------| +| **A) 서버 액션** | `actions.ts` | 기존 패턴 일관성 | blob → base64 변환 필요 | +| **B) API Proxy** | `app/api/proxy/` | `window.open` 간단 호출 | 프록시 라우트 추가 필요 | -```typescript -// actions.ts에 다운로드 함수 추가 -export async function downloadExportFile(exportId: number): Promise { - const response = await fetch( - `${API_URL}/api/v1/subscriptions/export/${exportId}/download`, - { - headers: { - 'Authorization': `Bearer ${token}`, // 기존 인증 헤더 패턴 참고 - 'X-API-KEY': apiKey, - }, - } - ); - - if (!response.ok) throw new Error('다운로드 실패'); - - const blob = await response.blob(); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `구독관리_${new Date().toISOString().slice(0, 10)}.xlsx`; - a.click(); - URL.revokeObjectURL(url); -} -``` +> 기존 프로젝트에서 파일 다운로드를 어떤 패턴으로 처리하는지 확인 후 선택한다. --- -## 4. Excel 파일 내용 +## 5. Excel 파일 내용 생성되는 Excel 파일의 컬럼 구조: @@ -212,24 +220,27 @@ export async function downloadExportFile(exportId: number): Promise { --- -## 5. 테스트 체크리스트 +## 6. 테스트 체크리스트 - [ ] "자료 내보내기" 버튼 클릭 → Excel 파일 다운로드 확인 - [ ] 다운로드된 파일 열기 → 데이터 정상 확인 - [ ] 연속 클릭 시 중복 차단 메시지 없이 재다운로드 가능 확인 - [ ] 구독 데이터 없는 테넌트에서 빈 Excel 생성 확인 +- [ ] `SubscriptionManagement.tsx`와 `SubscriptionClient.tsx` 양쪽 동작 확인 - [ ] 개발서버 URL: `https://dev.codebridge-x.com` → 설정 → 구독관리 --- -## 6. 관련 파일 +## 7. 관련 파일 | 위치 | 파일 | 설명 | |------|------|------| | React | `src/components/settings/SubscriptionManagement/actions.ts` | 서버 액션 (수정 대상) | -| React | `src/components/settings/SubscriptionManagement/SubscriptionClient.tsx` | 컴포넌트 (수정 대상) | -| API | `app/Http/Controllers/Api/V1/SubscriptionController.php` | 컨트롤러 | -| API | `app/Services/SubscriptionService.php` | 서비스 (동기 처리 로직) | +| React | `src/components/settings/SubscriptionManagement/SubscriptionManagement.tsx` | 메인 컴포넌트 (수정 대상) | +| React | `src/components/settings/SubscriptionManagement/SubscriptionClient.tsx` | 클라이언트 컴포넌트 (수정 대상) | +| API | `app/Http/Controllers/Api/V1/SubscriptionController.php` | 컨트롤러 (수정 완료) | +| API | `app/Services/SubscriptionService.php` | 서비스 — 동기 처리 로직 (수정 완료) | +| API | `routes/api/v1/finance.php` | 라우트 — 다운로드 엔드포인트 추가 (수정 완료) | ---