docs: [changes] 구독관리 내보내기 프론트엔드 수정 가이드 추가
This commit is contained in:
236
changes/20260318_subscription_export_frontend_guide.md
Normal file
236
changes/20260318_subscription_export_frontend_guide.md
Normal file
@@ -0,0 +1,236 @@
|
||||
# [프론트엔드] 구독관리 내보내기 API 변경 안내
|
||||
|
||||
**날짜:** 2026-03-18
|
||||
**작성자:** Claude Code
|
||||
**대상:** React 프론트엔드 개발자
|
||||
**배포:** 개발서버 반영 완료
|
||||
|
||||
---
|
||||
|
||||
## 1. 변경 배경
|
||||
|
||||
구독관리 페이지의 "자료 내보내기" 버튼 클릭 시 백엔드에서 `400 (이미 진행 중인 내보내기가 있습니다)` 오류가 반복 발생했다.
|
||||
|
||||
**원인**: 비동기 처리 Job이 미구현 상태로, `DataExport` 레코드가 `pending`에서 영원히 변경되지 않아 후속 요청이 차단되었다.
|
||||
|
||||
**수정**: 비동기 → **동기 처리**로 전환. `POST /export` 호출 시 즉시 Excel 파일이 생성되고, 응답에 `completed` 상태와 파일 정보가 포함된다.
|
||||
|
||||
---
|
||||
|
||||
## 2. API 변경 사항
|
||||
|
||||
### 2.1 변경된 엔드포인트
|
||||
|
||||
| Method | Path | 변경 내용 |
|
||||
|--------|------|----------|
|
||||
| `POST` | `/api/v1/subscriptions/export` | 응답에 `status: 'completed'` + 파일 정보 포함 (기존: `status: 'pending'`) |
|
||||
|
||||
### 2.2 신규 엔드포인트
|
||||
|
||||
| Method | Path | 설명 |
|
||||
|--------|------|------|
|
||||
| `GET` | `/api/v1/subscriptions/export/{id}/download` | 생성된 Excel 파일 다운로드 |
|
||||
|
||||
### 2.3 POST /export 응답 비교
|
||||
|
||||
**기존 응답** (status: pending, 파일 없음):
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "내보내기 요청이 접수되었습니다.",
|
||||
"data": {
|
||||
"id": 1,
|
||||
"status": "pending",
|
||||
"file_path": null,
|
||||
"file_name": null,
|
||||
"file_size": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**변경 후 응답** (status: completed, 파일 즉시 생성):
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "내보내기 요청이 접수되었습니다.",
|
||||
"data": {
|
||||
"id": 2,
|
||||
"status": "completed",
|
||||
"export_type": "all",
|
||||
"file_path": "exports/subscriptions_287_20260318_141023.xlsx",
|
||||
"file_name": "subscriptions_287_20260318_141023.xlsx",
|
||||
"file_size": 4523,
|
||||
"started_at": "2026-03-18T14:10:23.000000Z",
|
||||
"completed_at": "2026-03-18T14:10:23.000000Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **실패 시** `status: 'failed'`와 `error_message`가 포함된다.
|
||||
|
||||
---
|
||||
|
||||
## 3. 프론트엔드 수정 가이드
|
||||
|
||||
### 3.1 현재 코드 (수정 대상)
|
||||
|
||||
**파일:** `react/src/components/settings/SubscriptionManagement/actions.ts`
|
||||
|
||||
```typescript
|
||||
// 현재: POST 후 toast만 표시
|
||||
export async function requestDataExport(
|
||||
exportType: string = 'all'
|
||||
): Promise<ActionResult<{ id: number; status: string }>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/subscriptions/export`,
|
||||
method: 'POST',
|
||||
body: { export_type: exportType },
|
||||
transform: (data: { id: number; status: string }) => ({ id: data.id, status: data.status }),
|
||||
errorMessage: '내보내기 요청에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**파일:** `react/src/components/settings/SubscriptionManagement/SubscriptionClient.tsx`
|
||||
|
||||
```typescript
|
||||
// 현재: "완료되면 알림을 보내드립니다" (실제로 알림 없음)
|
||||
const handleExportData = useCallback(async () => {
|
||||
setIsExporting(true);
|
||||
try {
|
||||
const result = await requestDataExport('all');
|
||||
if (result.success) {
|
||||
toast.success('내보내기 요청이 등록되었습니다. 완료되면 알림을 보내드립니다.');
|
||||
} else {
|
||||
toast.error(result.error || '내보내기 요청에 실패했습니다.');
|
||||
}
|
||||
} catch (_error) {
|
||||
toast.error('서버 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setIsExporting(false);
|
||||
}
|
||||
}, []);
|
||||
```
|
||||
|
||||
### 3.2 수정 방향
|
||||
|
||||
POST 응답에서 `id`를 받아 **즉시 다운로드 URL로 리다이렉트**하는 방식으로 변경한다.
|
||||
|
||||
#### actions.ts 수정
|
||||
|
||||
```typescript
|
||||
// ===== 데이터 내보내기 요청 =====
|
||||
export async function requestDataExport(
|
||||
exportType: string = 'all'
|
||||
): Promise<ActionResult<{ id: number; status: string; file_name: string | null }>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/subscriptions/export`,
|
||||
method: 'POST',
|
||||
body: { export_type: exportType },
|
||||
transform: (data: { id: number; status: string; file_name: string | null }) => ({
|
||||
id: data.id,
|
||||
status: data.status,
|
||||
file_name: data.file_name,
|
||||
}),
|
||||
errorMessage: '내보내기 요청에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 내보내기 파일 다운로드 URL 생성 =====
|
||||
export function getExportDownloadUrl(exportId: number): string {
|
||||
return `${API_URL}/api/v1/subscriptions/export/${exportId}/download`;
|
||||
}
|
||||
```
|
||||
|
||||
#### SubscriptionClient.tsx 수정
|
||||
|
||||
```typescript
|
||||
const handleExportData = useCallback(async () => {
|
||||
setIsExporting(true);
|
||||
try {
|
||||
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');
|
||||
toast.success('Excel 파일이 다운로드됩니다.');
|
||||
} else if (result.data.status === 'failed') {
|
||||
toast.error('내보내기 처리 중 오류가 발생했습니다.');
|
||||
}
|
||||
} else {
|
||||
toast.error(result.error || '내보내기 요청에 실패했습니다.');
|
||||
}
|
||||
} catch (_error) {
|
||||
toast.error('서버 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setIsExporting(false);
|
||||
}
|
||||
}, []);
|
||||
```
|
||||
|
||||
> **참고**: 다운로드 URL은 인증이 필요한 API이므로, `window.open` 대신 `fetch` + `blob` 패턴이 필요할 수 있다. 기존 프로젝트의 파일 다운로드 패턴을 확인하여 적용한다.
|
||||
|
||||
### 3.3 인증이 필요한 경우 (fetch + blob 패턴)
|
||||
|
||||
API가 `auth:sanctum` 미들웨어 아래에 있으므로, 토큰 전달이 필요하다면:
|
||||
|
||||
```typescript
|
||||
// actions.ts에 다운로드 함수 추가
|
||||
export async function downloadExportFile(exportId: number): Promise<void> {
|
||||
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 파일 내용
|
||||
|
||||
생성되는 Excel 파일의 컬럼 구조:
|
||||
|
||||
| No | 요금제 | 요금제 코드 | 월 요금 | 결제주기 | 시작일 | 종료일 | 상태 | 취소일 | 취소 사유 |
|
||||
|----|--------|-----------|---------|---------|--------|--------|------|--------|----------|
|
||||
| 1 | 스탠다드 | STD | 50,000 | 월간 | 2026-01-01 | 2026-02-01 | 활성 | - | - |
|
||||
|
||||
---
|
||||
|
||||
## 5. 테스트 체크리스트
|
||||
|
||||
- [ ] "자료 내보내기" 버튼 클릭 → Excel 파일 다운로드 확인
|
||||
- [ ] 다운로드된 파일 열기 → 데이터 정상 확인
|
||||
- [ ] 연속 클릭 시 중복 차단 메시지 없이 재다운로드 가능 확인
|
||||
- [ ] 구독 데이터 없는 테넌트에서 빈 Excel 생성 확인
|
||||
- [ ] 개발서버 URL: `https://dev.codebridge-x.com` → 설정 → 구독관리
|
||||
|
||||
---
|
||||
|
||||
## 6. 관련 파일
|
||||
|
||||
| 위치 | 파일 | 설명 |
|
||||
|------|------|------|
|
||||
| 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` | 서비스 (동기 처리 로직) |
|
||||
|
||||
---
|
||||
|
||||
**최종 업데이트**: 2026-03-18
|
||||
Reference in New Issue
Block a user