// app/api/rfq-attachments/download/route.ts import { NextRequest, NextResponse } from 'next/server'; import { readFile, access, constants } from 'fs/promises'; import { join } from 'path'; import db from '@/db/db'; import { bRfqAttachmentRevisions, vendorResponseAttachmentsB } from '@/db/schema'; import { eq, or } from 'drizzle-orm'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/app/api/auth/[...nextauth]/route'; export async function GET(request: NextRequest) { try { // 세션 확인 const session = await getServerSession(authOptions); if (!session?.user) { return NextResponse.json( { error: "Unauthorized" }, { status: 401 } ); } // 파라미터 추출 const path = request.nextUrl.searchParams.get("path"); const type = request.nextUrl.searchParams.get("type"); // "client" | "vendor" const revisionId = request.nextUrl.searchParams.get("revisionId"); const responseFileId = request.nextUrl.searchParams.get("responseFileId"); if (!path) { return NextResponse.json( { error: "File path is required" }, { status: 400 } ); } // DB에서 파일 정보 조회 let dbRecord = null; if (type === "client" && revisionId) { // 발주처 첨부파일 리비전 const [record] = await db .select({ fileName: bRfqAttachmentRevisions.fileName, originalFileName: bRfqAttachmentRevisions.originalFileName, filePath: bRfqAttachmentRevisions.filePath, fileSize: bRfqAttachmentRevisions.fileSize, fileType: bRfqAttachmentRevisions.fileType, }) .from(bRfqAttachmentRevisions) .where(eq(bRfqAttachmentRevisions.id, Number(revisionId))); dbRecord = record; } else if (type === "vendor" && responseFileId) { // 벤더 응답 파일 const [record] = await db .select({ fileName: vendorResponseAttachmentsB.fileName, originalFileName: vendorResponseAttachmentsB.originalFileName, filePath: vendorResponseAttachmentsB.filePath, fileSize: vendorResponseAttachmentsB.fileSize, fileType: vendorResponseAttachmentsB.fileType, }) .from(vendorResponseAttachmentsB) .where(eq(vendorResponseAttachmentsB.id, Number(responseFileId))); dbRecord = record; } else { // filePath로 직접 검색 (fallback) const [clientRecord] = await db .select({ fileName: bRfqAttachmentRevisions.fileName, originalFileName: bRfqAttachmentRevisions.originalFileName, filePath: bRfqAttachmentRevisions.filePath, fileSize: bRfqAttachmentRevisions.fileSize, fileType: bRfqAttachmentRevisions.fileType, }) .from(bRfqAttachmentRevisions) .where(eq(bRfqAttachmentRevisions.filePath, path)); if (clientRecord) { dbRecord = clientRecord; } else { // 벤더 파일에서도 검색 const [vendorRecord] = await db .select({ fileName: vendorResponseAttachmentsB.fileName, originalFileName: vendorResponseAttachmentsB.originalFileName, filePath: vendorResponseAttachmentsB.filePath, fileSize: vendorResponseAttachmentsB.fileSize, fileType: vendorResponseAttachmentsB.fileType, }) .from(vendorResponseAttachmentsB) .where(eq(vendorResponseAttachmentsB.filePath, path)); dbRecord = vendorRecord; } } // 파일 정보 설정 let fileName; let fileType; if (dbRecord) { // DB에서 찾은 경우 원본 파일명 사용 fileName = dbRecord.originalFileName || dbRecord.fileName; fileType = dbRecord.fileType; console.log("✅ DB에서 파일 정보 찾음:", { fileName, fileType, path: dbRecord.filePath }); } else { // DB에서 찾지 못한 경우 경로에서 파일명 추출 fileName = path.split('/').pop() || 'download'; console.log("⚠️ DB에서 파일 정보를 찾지 못함, 경로에서 추출:", fileName); } // 파일 경로 구성 const storedPath = path.replace(/^\/+/, ""); // 앞쪽 슬래시 제거 // 가능한 파일 경로들 const possiblePaths = [ join(process.cwd(), "public", storedPath), join(process.cwd(), "uploads", storedPath), join(process.cwd(), "storage", storedPath), join(process.cwd(), storedPath), // 절대 경로인 경우 ]; // 실제 파일 찾기 let actualPath = null; for (const testPath of possiblePaths) { try { await access(testPath, constants.R_OK); actualPath = testPath; console.log("✅ 파일 발견:", testPath); break; } catch (err) { // 조용히 다음 경로 시도 } } if (!actualPath) { console.error("❌ 모든 경로에서 파일을 찾을 수 없음:", possiblePaths); return NextResponse.json( { error: "File not found on server", details: { path: path, fileName: fileName, triedPaths: possiblePaths } }, { status: 404 } ); } // 파일 읽기 const fileBuffer = await readFile(actualPath); // MIME 타입 결정 const fileExtension = fileName.split('.').pop()?.toLowerCase() || ''; let contentType = fileType || 'application/octet-stream'; // DB의 fileType 우선 사용 // 확장자에 따른 MIME 타입 매핑 (fallback) if (!contentType || contentType === 'application/octet-stream') { const mimeTypes: Record = { 'pdf': 'application/pdf', 'doc': 'application/msword', 'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'xls': 'application/vnd.ms-excel', 'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'ppt': 'application/vnd.ms-powerpoint', 'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'txt': 'text/plain', 'csv': 'text/csv', 'png': 'image/png', 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'gif': 'image/gif', 'bmp': 'image/bmp', 'svg': 'image/svg+xml', 'dwg': 'application/acad', 'dxf': 'application/dxf', 'zip': 'application/zip', 'rar': 'application/x-rar-compressed', '7z': 'application/x-7z-compressed', }; contentType = mimeTypes[fileExtension] || 'application/octet-stream'; } // 안전한 파일명 생성 (특수문자 처리) const safeFileName = fileName.replace(/[^\w\s.-]/gi, '_'); // 다운로드용 헤더 설정 const headers = new Headers(); headers.set('Content-Type', contentType); headers.set('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(safeFileName)}`); headers.set('Content-Length', fileBuffer.length.toString()); headers.set('Cache-Control', 'no-cache, no-store, must-revalidate'); headers.set('Pragma', 'no-cache'); headers.set('Expires', '0'); console.log("✅ 파일 다운로드 성공:", { fileName: safeFileName, contentType, size: fileBuffer.length, actualPath }); return new NextResponse(fileBuffer, { status: 200, headers, }); } catch (error) { console.error('❌ RFQ 첨부파일 다운로드 오류:', error); return NextResponse.json( { error: 'Failed to download file', details: error instanceof Error ? error.message : String(error) }, { status: 500 } ); } }