summaryrefslogtreecommitdiff
path: root/lib/file-download.ts
diff options
context:
space:
mode:
Diffstat (limited to 'lib/file-download.ts')
-rw-r--r--lib/file-download.ts260
1 files changed, 260 insertions, 0 deletions
diff --git a/lib/file-download.ts b/lib/file-download.ts
new file mode 100644
index 00000000..1e8536b5
--- /dev/null
+++ b/lib/file-download.ts
@@ -0,0 +1,260 @@
+// lib/file-download.ts
+// 공용 파일 다운로드 유틸리티
+
+import { toast } from "sonner";
+
+/**
+ * 파일 타입 정보
+ */
+export interface FileInfo {
+ type: 'pdf' | 'document' | 'spreadsheet' | 'image' | 'archive' | 'other';
+ canPreview: boolean;
+ icon: string;
+ mimeType?: string;
+}
+
+/**
+ * 파일 정보 가져오기
+ */
+export const getFileInfo = (fileName: string): FileInfo => {
+ const ext = fileName.toLowerCase().split('.').pop();
+
+ const fileTypes: Record<string, FileInfo> = {
+ pdf: { type: 'pdf', canPreview: true, icon: '📄', mimeType: 'application/pdf' },
+ doc: { type: 'document', canPreview: false, icon: '📝', mimeType: 'application/msword' },
+ docx: { type: 'document', canPreview: false, icon: '📝', mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' },
+ xls: { type: 'spreadsheet', canPreview: false, icon: '📊', mimeType: 'application/vnd.ms-excel' },
+ xlsx: { type: 'spreadsheet', canPreview: false, icon: '📊', mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' },
+ ppt: { type: 'document', canPreview: false, icon: '📑', mimeType: 'application/vnd.ms-powerpoint' },
+ pptx: { type: 'document', canPreview: false, icon: '📑', mimeType: 'application/vnd.openxmlformats-officedocument.presentationml.presentation' },
+ jpg: { type: 'image', canPreview: true, icon: '🖼️', mimeType: 'image/jpeg' },
+ jpeg: { type: 'image', canPreview: true, icon: '🖼️', mimeType: 'image/jpeg' },
+ png: { type: 'image', canPreview: true, icon: '🖼️', mimeType: 'image/png' },
+ gif: { type: 'image', canPreview: true, icon: '🖼️', mimeType: 'image/gif' },
+ webp: { type: 'image', canPreview: true, icon: '🖼️', mimeType: 'image/webp' },
+ svg: { type: 'image', canPreview: true, icon: '🖼️', mimeType: 'image/svg+xml' },
+ zip: { type: 'archive', canPreview: false, icon: '📦', mimeType: 'application/zip' },
+ rar: { type: 'archive', canPreview: false, icon: '📦', mimeType: 'application/x-rar-compressed' },
+ '7z': { type: 'archive', canPreview: false, icon: '📦', mimeType: 'application/x-7z-compressed' },
+ txt: { type: 'document', canPreview: true, icon: '📝', mimeType: 'text/plain' },
+ csv: { type: 'spreadsheet', canPreview: true, icon: '📊', mimeType: 'text/csv' },
+ };
+
+ return fileTypes[ext || ''] || { type: 'other', canPreview: false, icon: '📎', mimeType: 'application/octet-stream' };
+};
+
+/**
+ * 파일 크기를 읽기 쉬운 형태로 변환
+ */
+export const formatFileSize = (bytes: number): string => {
+ if (bytes === 0) return '0 Bytes';
+
+ const k = 1024;
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
+};
+
+/**
+ * 파일 다운로드 옵션
+ */
+export interface FileDownloadOptions {
+ /** 다운로드 액션 타입 */
+ action?: 'download' | 'preview';
+ /** 에러 시 토스트 표시 여부 */
+ showToast?: boolean;
+ /** 성공 시 토스트 표시 여부 */
+ showSuccessToast?: boolean;
+ /** 커스텀 에러 핸들러 */
+ onError?: (error: string) => void;
+ /** 커스텀 성공 핸들러 */
+ onSuccess?: (fileName: string, fileSize?: number) => void;
+ /** 진행률 콜백 (큰 파일용) */
+ onProgress?: (progress: number) => void;
+}
+
+/**
+ * 파일 다운로드 결과
+ */
+export interface FileDownloadResult {
+ success: boolean;
+ error?: string;
+ fileSize?: number;
+ fileInfo?: FileInfo;
+}
+
+/**
+ * 파일 메타데이터 확인
+ */
+export const checkFileMetadata = async (url: string): Promise<{
+ exists: boolean;
+ size?: number;
+ contentType?: string;
+ lastModified?: Date;
+ error?: string;
+}> => {
+ try {
+ const response = await fetch(url, {
+ method: 'HEAD',
+ headers: { 'Cache-Control': 'no-cache' }
+ });
+
+ if (!response.ok) {
+ let error = "파일 접근 실패";
+
+ switch (response.status) {
+ case 404:
+ error = "파일을 찾을 수 없습니다";
+ break;
+ case 403:
+ error = "파일 접근 권한이 없습니다";
+ break;
+ case 500:
+ error = "서버 오류가 발생했습니다";
+ break;
+ default:
+ error = `파일 접근 실패 (${response.status})`;
+ }
+
+ return { exists: false, error };
+ }
+
+ const contentLength = response.headers.get('Content-Length');
+ const contentType = response.headers.get('Content-Type');
+ const lastModified = response.headers.get('Last-Modified');
+
+ return {
+ exists: true,
+ size: contentLength ? parseInt(contentLength, 10) : undefined,
+ contentType: contentType || undefined,
+ lastModified: lastModified ? new Date(lastModified) : undefined,
+ };
+ } catch (error) {
+ return {
+ exists: false,
+ error: error instanceof Error ? error.message : "네트워크 오류가 발생했습니다"
+ };
+ }
+};
+
+/**
+ * 메인 파일 다운로드 함수
+ */
+export const downloadFile = async (
+ filePath: string,
+ fileName: string,
+ options: FileDownloadOptions = {}
+): Promise<FileDownloadResult> => {
+ const { action = 'download', showToast = true, onError, onSuccess } = options;
+
+ try {
+ // ✅ URL에 다운로드 강제 파라미터 추가
+ const baseUrl = filePath.startsWith('http')
+ ? filePath
+ : `${window.location.origin}${filePath}`;
+
+ const url = new URL(baseUrl);
+ if (action === 'download') {
+ url.searchParams.set('download', 'true'); // 🔑 핵심!
+ }
+
+ const fullUrl = url.toString();
+
+ // 파일 정보 확인
+ const metadata = await checkFileMetadata(fullUrl);
+ if (!metadata.exists) {
+ const error = metadata.error || "파일을 찾을 수 없습니다";
+ if (showToast) toast.error(error);
+ if (onError) onError(error);
+ return { success: false, error };
+ }
+
+ const fileInfo = getFileInfo(fileName);
+
+ // 미리보기 처리 (download=true 없이)
+ if (action === 'preview' && fileInfo.canPreview) {
+ const previewUrl = filePath.startsWith('http')
+ ? filePath
+ : `${window.location.origin}${filePath}`;
+
+ window.open(previewUrl, '_blank', 'noopener,noreferrer');
+ if (showToast) toast.success(`${fileInfo.icon} 파일을 새 탭에서 열었습니다`);
+ if (onSuccess) onSuccess(fileName, metadata.size);
+ return { success: true, fileSize: metadata.size, fileInfo };
+ }
+
+ // ✅ 안전한 다운로드 방식 (fetch + Blob)
+ console.log(`📥 안전한 다운로드: ${fullUrl}`);
+
+ const response = await fetch(fullUrl);
+ if (!response.ok) {
+ throw new Error(`다운로드 실패: ${response.status}`);
+ }
+
+ // Blob으로 변환
+ const blob = await response.blob();
+
+ // ✅ 브라우저 호환성을 고려한 다운로드
+ const downloadUrl = URL.createObjectURL(blob);
+
+ const link = document.createElement('a');
+ link.href = downloadUrl;
+ link.download = fileName;
+ link.style.display = 'none'; // 화면에 표시되지 않도록
+
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+
+ // 메모리 정리 (중요!)
+ setTimeout(() => URL.revokeObjectURL(downloadUrl), 100);
+
+ // 성공 처리
+ if (showToast) {
+ const sizeText = metadata.size ? ` (${formatFileSize(metadata.size)})` : '';
+ toast.success(`${fileInfo.icon} 파일 다운로드 완료: ${fileName}${sizeText}`);
+ }
+ if (onSuccess) onSuccess(fileName, metadata.size);
+
+ return { success: true, fileSize: metadata.size, fileInfo };
+
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : "파일 처리 중 오류가 발생했습니다";
+ console.error("❌ 다운로드 오류:", error);
+ if (showToast) toast.error(errorMessage);
+ if (onError) onError(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+};
+
+/**
+ * 간편 다운로드 함수
+ */
+export const quickDownload = (filePath: string, fileName: string) => {
+ return downloadFile(filePath, fileName, { action: 'download' });
+};
+
+/**
+ * 간편 미리보기 함수
+ */
+export const quickPreview = (filePath: string, fileName: string) => {
+ const fileInfo = getFileInfo(fileName);
+
+ if (!fileInfo.canPreview) {
+ toast.warning("이 파일 형식은 미리보기를 지원하지 않습니다. 다운로드를 진행합니다.");
+ return downloadFile(filePath, fileName, { action: 'download' });
+ }
+
+ return downloadFile(filePath, fileName, { action: 'preview' });
+};
+
+/**
+ * 파일 다운로드 또는 미리보기 (자동 판단)
+ */
+export const smartFileAction = (filePath: string, fileName: string) => {
+ const fileInfo = getFileInfo(fileName);
+ const action = fileInfo.canPreview ? 'preview' : 'download';
+
+ return downloadFile(filePath, fileName, { action });
+}; \ No newline at end of file