summaryrefslogtreecommitdiff
path: root/app/api/data-room
diff options
context:
space:
mode:
Diffstat (limited to 'app/api/data-room')
-rw-r--r--app/api/data-room/[projectId]/[fileId]/download/route.ts246
-rw-r--r--app/api/data-room/[projectId]/[fileId]/route.ts147
-rw-r--r--app/api/data-room/[projectId]/download-folder/[folderId]/route.ts289
-rw-r--r--app/api/data-room/[projectId]/download-multiple/route.ts162
-rw-r--r--app/api/data-room/[projectId]/permissions/route.ts74
-rw-r--r--app/api/data-room/[projectId]/route.ts118
-rw-r--r--app/api/data-room/[projectId]/share/[token]/route.ts45
-rw-r--r--app/api/data-room/[projectId]/share/route.ts79
-rw-r--r--app/api/data-room/[projectId]/upload/route.ts139
9 files changed, 1299 insertions, 0 deletions
diff --git a/app/api/data-room/[projectId]/[fileId]/download/route.ts b/app/api/data-room/[projectId]/[fileId]/download/route.ts
new file mode 100644
index 00000000..3a3a8fdd
--- /dev/null
+++ b/app/api/data-room/[projectId]/[fileId]/download/route.ts
@@ -0,0 +1,246 @@
+// app/api/data-room/[projectId]/[fileId]/download/route.ts
+import { NextRequest, NextResponse } from 'next/server';
+import { getServerSession } from 'next-auth/next';
+import { authOptions } from '@/app/api/auth/[...nextauth]/route';
+import { FileService, type FileAccessContext } from '@/lib/services/fileService';
+import { promises as fs } from 'fs';
+import path from 'path';
+import db from "@/db/db";
+import { fileItems } from "@/db/schema/fileSystem";
+import { eq } from "drizzle-orm";
+
+export async function GET(
+ request: NextRequest,
+ { params }: { params: { projectId: string; fileId: string } }
+) {
+ try {
+ const session = await getServerSession(authOptions);
+ if (!session?.user) {
+ return NextResponse.json({ error: '인증이 필요합니다' }, { status: 401 });
+ }
+
+ const context: FileAccessContext = {
+ userId: Number(session.user.id),
+ userDomain: session.user.domain || 'partners',
+ userEmail: session.user.email,
+ ipAddress: request.ip || request.headers.get('x-forwarded-for') || undefined,
+ userAgent: request.headers.get('user-agent') || undefined,
+ };
+
+ const fileService = new FileService();
+
+ // 파일 접근 권한 확인
+ const hasAccess = await fileService.checkFileAccess(
+ params.fileId,
+ context,
+ 'download'
+ );
+
+ if (!hasAccess) {
+ return NextResponse.json(
+ { error: '파일 다운로드 권한이 없습니다' },
+ { status: 403 }
+ );
+ }
+
+ // FileService를 통해 파일 정보 가져오기 (다운로드 카운트 증가 및 로그 기록)
+ const file = await fileService.downloadFile(params.fileId, context);
+
+ if (!file) {
+ return NextResponse.json(
+ { error: '파일을 찾을 수 없습니다' },
+ { status: 404 }
+ );
+ }
+
+ // 파일 경로 확인
+ if (!file.filePath) {
+ return NextResponse.json(
+ { error: '파일 경로가 없습니다' },
+ { status: 404 }
+ );
+ }
+
+ // 실제 파일 경로 구성
+ const nasPath = process.env.NAS_PATH || "/evcp_nas";
+ const isProduction = process.env.NODE_ENV === "production";
+
+ let absolutePath: string;
+ if (isProduction) {
+ // 프로덕션: NAS 경로 사용
+ const relativePath = file.filePath.replace('/api/files/', '');
+ absolutePath = path.join(nasPath, relativePath);
+ } else {
+ // 개발: public 폴더 사용
+ absolutePath = path.join(process.cwd(), 'public', file.filePath);
+ }
+
+ // 파일 존재 여부 확인
+ try {
+ await fs.access(absolutePath);
+ } catch (error) {
+ console.error('파일을 찾을 수 없습니다:', absolutePath);
+ return NextResponse.json(
+ { error: '파일을 찾을 수 없습니다' },
+ { status: 404 }
+ );
+ }
+
+ // 파일 읽기
+ const fileBuffer = await fs.readFile(absolutePath);
+
+ // MIME 타입 결정
+ const mimeType = getMimeType(file.name, file.mimeType);
+
+ // 파일명 인코딩 (한글 등 특수문자 처리)
+ const encodedFileName = encodeURIComponent(file.name);
+
+ // Response Headers 설정
+ const headers = new Headers();
+ headers.set('Content-Type', mimeType);
+ headers.set('Content-Length', fileBuffer.length.toString());
+ headers.set('Content-Disposition', `attachment; filename*=UTF-8''${encodedFileName}`);
+ headers.set('Cache-Control', 'no-cache, no-store, must-revalidate');
+ headers.set('Pragma', 'no-cache');
+ headers.set('Expires', '0');
+
+ // 보안 헤더 추가
+ headers.set('X-Content-Type-Options', 'nosniff');
+ headers.set('X-Frame-Options', 'DENY');
+ headers.set('X-XSS-Protection', '1; mode=block');
+
+ // 파일 스트림 반환
+ return new NextResponse(fileBuffer, {
+ status: 200,
+ headers,
+ });
+
+ } catch (error) {
+ console.error('파일 다운로드 오류:', error);
+
+ if (error instanceof Error) {
+ if (error.message.includes('권한')) {
+ return NextResponse.json(
+ { error: error.message },
+ { status: 403 }
+ );
+ }
+ }
+
+ return NextResponse.json(
+ { error: '파일 다운로드에 실패했습니다' },
+ { status: 500 }
+ );
+ }
+}
+
+// HEAD 요청 처리 (파일 정보만 확인)
+export async function HEAD(
+ request: NextRequest,
+ { params }: { params: { projectId: string; fileId: string } }
+) {
+ try {
+ const session = await getServerSession(authOptions);
+ if (!session?.user) {
+ return new NextResponse(null, { status: 401 });
+ }
+
+ const context: FileAccessContext = {
+ userId: Number(session.user.id),
+ userDomain: session.user.domain || 'partners',
+ userEmail: session.user.email,
+ ipAddress: request.ip || request.headers.get('x-forwarded-for') || undefined,
+ userAgent: request.headers.get('user-agent') || undefined,
+ };
+
+ const fileService = new FileService();
+
+ // 파일 접근 권한 확인
+ const hasAccess = await fileService.checkFileAccess(
+ params.fileId,
+ context,
+ 'view' // HEAD 요청은 view 권한만 확인
+ );
+
+ if (!hasAccess) {
+ return new NextResponse(null, { status: 403 });
+ }
+
+ // 파일 정보 조회
+ const file = await db.query.fileItems.findFirst({
+ where: eq(fileItems.id, params.fileId),
+ });
+
+ if (!file || !file.filePath) {
+ return new NextResponse(null, { status: 404 });
+ }
+
+ const headers = new Headers();
+ headers.set('Content-Type', getMimeType(file.name, file.mimeType));
+ headers.set('Content-Length', file.size?.toString() || '0');
+ headers.set('Last-Modified', new Date(file.updatedAt).toUTCString());
+
+ return new NextResponse(null, {
+ status: 200,
+ headers,
+ });
+
+ } catch (error) {
+ console.error('HEAD 요청 오류:', error);
+ return new NextResponse(null, { status: 500 });
+ }
+}
+
+// MIME 타입 결정 헬퍼 함수
+function getMimeType(fileName: string, storedMimeType?: string | null): string {
+ // DB에 저장된 MIME 타입이 있으면 우선 사용
+ if (storedMimeType) {
+ return storedMimeType;
+ }
+
+ // 확장자 기반 MIME 타입 매핑
+ const ext = path.extname(fileName).toLowerCase().substring(1);
+ const mimeTypes: Record<string, string> = {
+ // Documents
+ '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',
+
+ // Images
+ 'jpg': 'image/jpeg',
+ 'jpeg': 'image/jpeg',
+ 'png': 'image/png',
+ 'gif': 'image/gif',
+ 'bmp': 'image/bmp',
+ 'webp': 'image/webp',
+ 'svg': 'image/svg+xml',
+
+ // Archives
+ 'zip': 'application/zip',
+ 'rar': 'application/x-rar-compressed',
+ '7z': 'application/x-7z-compressed',
+
+ // CAD
+ 'dwg': 'application/x-dwg',
+ 'dxf': 'application/x-dxf',
+
+ // Video
+ 'mp4': 'video/mp4',
+ 'avi': 'video/x-msvideo',
+ 'mov': 'video/quicktime',
+ 'wmv': 'video/x-ms-wmv',
+
+ // Audio
+ 'mp3': 'audio/mpeg',
+ 'wav': 'audio/wav',
+ 'ogg': 'audio/ogg',
+ };
+
+ return mimeTypes[ext] || 'application/octet-stream';
+} \ No newline at end of file
diff --git a/app/api/data-room/[projectId]/[fileId]/route.ts b/app/api/data-room/[projectId]/[fileId]/route.ts
new file mode 100644
index 00000000..176aaf63
--- /dev/null
+++ b/app/api/data-room/[projectId]/[fileId]/route.ts
@@ -0,0 +1,147 @@
+// app/api/files/[projectId]/[fileId]/route.ts
+import { NextRequest, NextResponse } from 'next/server';
+import { getServerSession } from 'next-auth/next';
+import { authOptions } from '@/app/api/auth/[...nextauth]/route'
+import { FileService, type FileAccessContext } from '@/lib/services/fileService';
+
+// 파일 정보 조회
+export async function GET(
+ request: NextRequest,
+ { params }: { params: { projectId: string; fileId: string } }
+) {
+ try {
+ const session = await getServerSession(authOptions);
+ if (!session?.user) {
+ return NextResponse.json({ error: '인증이 필요합니다' }, { status: 401 });
+ }
+
+ const context: FileAccessContext = {
+ userId: session.user.id,
+ userDomain: session.user.domain || 'partners',
+ userEmail: session.user.email,
+ ipAddress: request.ip || request.headers.get('x-forwarded-for') || undefined,
+ userAgent: request.headers.get('user-agent') || undefined,
+ };
+
+ const fileService = new FileService();
+ const hasAccess = await fileService.checkFileAccess(
+ params.fileId,
+ context,
+ 'view'
+ );
+
+ if (!hasAccess) {
+ return NextResponse.json(
+ { error: '파일 접근 권한이 없습니다' },
+ { status: 403 }
+ );
+ }
+
+ // 파일 정보 반환
+ const file = await fileService.downloadFile(params.fileId, context);
+
+ if (!file) {
+ return NextResponse.json(
+ { error: '파일을 찾을 수 없습니다' },
+ { status: 404 }
+ );
+ }
+
+ return NextResponse.json(file);
+ } catch (error) {
+ console.error('파일 조회 오류:', error);
+ return NextResponse.json(
+ { error: '파일 조회에 실패했습니다' },
+ { status: 500 }
+ );
+ }
+}
+
+// 파일 수정
+export async function PATCH(
+ request: NextRequest,
+ { params }: { params: { projectId: string; fileId: string } }
+) {
+ try {
+ const session = await getServerSession(authOptions);
+ if (!session?.user) {
+ return NextResponse.json({ error: '인증이 필요합니다' }, { status: 401 });
+ }
+
+ const context: FileAccessContext = {
+ userId: session.user.id,
+ userDomain: session.user.domain || 'partners',
+ userEmail: session.user.email,
+ ipAddress: request.ip || request.headers.get('x-forwarded-for') || undefined,
+ userAgent: request.headers.get('user-agent') || undefined,
+ };
+
+ const fileService = new FileService();
+ const hasAccess = await fileService.checkFileAccess(
+ params.fileId,
+ context,
+ 'edit'
+ );
+
+ if (!hasAccess) {
+ return NextResponse.json(
+ { error: '파일 수정 권한이 없습니다' },
+ { status: 403 }
+ );
+ }
+
+ const body = await request.json();
+
+ // 파일 이동 처리
+ if (body.parentId !== undefined) {
+ await fileService.moveFile(params.fileId, body.parentId, context);
+ }
+
+ return NextResponse.json({ success: true });
+ } catch (error) {
+ console.error('파일 수정 오류:', error);
+ return NextResponse.json(
+ { error: '파일 수정에 실패했습니다' },
+ { status: 500 }
+ );
+ }
+}
+
+// 파일 삭제
+export async function DELETE(
+ request: NextRequest,
+ { params }: { params: { projectId: string; fileId: string } }
+) {
+ try {
+ const session = await getServerSession(authOptions);
+ if (!session?.user) {
+ return NextResponse.json({ error: '인증이 필요합니다' }, { status: 401 });
+ }
+
+ const context: FileAccessContext = {
+ userId: session.user.id,
+ userDomain: session.user.domain || 'partners',
+ userEmail: session.user.email,
+ ipAddress: request.ip || request.headers.get('x-forwarded-for') || undefined,
+ userAgent: request.headers.get('user-agent') || undefined,
+ };
+
+ const fileService = new FileService();
+ await fileService.deleteFile(params.fileId, context);
+
+ return NextResponse.json({ success: true });
+ } catch (error) {
+ if (error instanceof Error && error.message.includes('권한')) {
+ return NextResponse.json(
+ { error: error.message },
+ { status: 403 }
+ );
+ }
+
+ console.error('파일 삭제 오류:', error);
+ return NextResponse.json(
+ { error: '파일 삭제에 실패했습니다' },
+ { status: 500 }
+ );
+ }
+} \ No newline at end of file
diff --git a/app/api/data-room/[projectId]/download-folder/[folderId]/route.ts b/app/api/data-room/[projectId]/download-folder/[folderId]/route.ts
new file mode 100644
index 00000000..bba7066f
--- /dev/null
+++ b/app/api/data-room/[projectId]/download-folder/[folderId]/route.ts
@@ -0,0 +1,289 @@
+// app/api/data-room/[projectId]/download-folder/[folderId]/route.ts
+import { NextRequest, NextResponse } from 'next/server';
+import { getServerSession } from 'next-auth/next';
+import { authOptions } from '@/app/api/auth/[...nextauth]/route';
+import { FileService, type FileAccessContext } from '@/lib/services/fileService';
+import { promises as fs } from 'fs';
+import path from 'path';
+import archiver from 'archiver';
+import db from "@/db/db";
+import { fileItems } from "@/db/schema/fileSystem";
+import { eq, and } from "drizzle-orm";
+
+interface FileWithPath {
+ file: any;
+ absolutePath: string;
+ relativePath: string;
+}
+
+export async function GET(
+ request: NextRequest,
+ { params }: { params: { projectId: string; folderId: string } }
+) {
+ try {
+ const session = await getServerSession(authOptions);
+ if (!session?.user) {
+ return NextResponse.json({ error: '인증이 필요합니다' }, { status: 401 });
+ }
+
+ const context: FileAccessContext = {
+ userId: Number(session.user.id),
+ userDomain: session.user.domain || 'partners',
+ userEmail: session.user.email,
+ ipAddress: request.ip || request.headers.get('x-forwarded-for') || undefined,
+ userAgent: request.headers.get('user-agent') || undefined,
+ };
+
+ // 폴더 정보 가져오기
+ const folder = await db.query.fileItems.findFirst({
+ where: and(
+ eq(fileItems.id, params.folderId),
+ eq(fileItems.projectId, params.projectId)
+ ),
+ });
+
+ if (!folder || folder.type !== 'folder') {
+ return NextResponse.json(
+ { error: '폴더를 찾을 수 없습니다' },
+ { status: 404 }
+ );
+ }
+
+ const fileService = new FileService();
+ const downloadableFiles: FileWithPath[] = [];
+ const unauthorizedFiles: string[] = [];
+
+ // 재귀적으로 폴더 내 모든 파일 가져오기 및 권한 확인
+ const processFolder = async (
+ folderId: string,
+ folderPath: string = ''
+ ): Promise<void> => {
+ const items = await db.query.fileItems.findMany({
+ where: and(
+ eq(fileItems.parentId, folderId),
+ eq(fileItems.projectId, params.projectId)
+ ),
+ });
+
+ for (const item of items) {
+ if (item.type === 'file') {
+ // 파일 권한 확인
+ const hasAccess = await fileService.checkFileAccess(
+ item.id,
+ context,
+ 'download'
+ );
+
+ if (!hasAccess) {
+ // 권한이 없는 파일 기록
+ unauthorizedFiles.push(path.join(folderPath, item.name));
+ continue;
+ }
+
+ if (!item.filePath) continue;
+
+ // 실제 파일 경로 구성
+ const nasPath = process.env.NAS_PATH || "/evcp_nas";
+ const isProduction = process.env.NODE_ENV === "production";
+
+ let absolutePath: string;
+ if (isProduction) {
+ const relativePath = item.filePath.replace('/api/files/', '');
+ absolutePath = path.join(nasPath, relativePath);
+ } else {
+ absolutePath = path.join(process.cwd(), 'public', item.filePath);
+ }
+
+ // 파일 존재 여부 확인
+ try {
+ await fs.access(absolutePath);
+ downloadableFiles.push({
+ file: item,
+ absolutePath,
+ relativePath: path.join(folderPath, item.name)
+ });
+
+ // 다운로드 카운트 증가 및 로그 기록
+ await fileService.downloadFile(item.id, context);
+ } catch (error) {
+ console.warn(`파일을 찾을 수 없습니다: ${absolutePath}`);
+ }
+ } else if (item.type === 'folder') {
+ // 하위 폴더 재귀 처리
+ await processFolder(
+ item.id,
+ path.join(folderPath, item.name)
+ );
+ }
+ }
+ };
+
+ // 폴더 처리 시작
+ await processFolder(params.folderId, folder.name);
+
+ // 권한이 없는 파일이 있으면 다운로드 차단
+ if (unauthorizedFiles.length > 0) {
+ return NextResponse.json(
+ {
+ error: '일부 파일에 대한 다운로드 권한이 없습니다',
+ unauthorizedFiles: unauthorizedFiles,
+ unauthorizedCount: unauthorizedFiles.length,
+ message: `다음 파일들에 대한 권한이 없어 폴더 다운로드가 취소되었습니다: ${unauthorizedFiles.slice(0, 5).join(', ')}${unauthorizedFiles.length > 5 ? ` 외 ${unauthorizedFiles.length - 5}개` : ''}`
+ },
+ { status: 403 }
+ );
+ }
+
+ // 다운로드할 파일이 없는 경우
+ if (downloadableFiles.length === 0) {
+ return NextResponse.json(
+ { error: '다운로드 가능한 파일이 없습니다' },
+ { status: 404 }
+ );
+ }
+
+ // 파일 크기 합계 체크 (최대 500MB)
+ const totalSize = downloadableFiles.reduce((sum, item) =>
+ sum + (item.file.size || 0), 0
+ );
+
+ const maxSize = 500 * 1024 * 1024; // 500MB
+ if (totalSize > maxSize) {
+ return NextResponse.json(
+ {
+ error: `폴더 크기가 너무 큽니다 (${(totalSize / 1024 / 1024).toFixed(2)}MB). 최대 500MB까지 다운로드 가능합니다.`,
+ totalSize: totalSize,
+ maxSize: maxSize,
+ fileCount: downloadableFiles.length
+ },
+ { status: 400 }
+ );
+ }
+
+ console.log(`📦 폴더 다운로드 시작: ${folder.name} (${downloadableFiles.length}개 파일, ${(totalSize / 1024 / 1024).toFixed(2)}MB)`);
+
+ // ZIP 스트림 생성
+ const archive = archiver('zip', {
+ zlib: { level: 5 } // 압축 레벨
+ });
+
+ // 스트림을 Response로 변환
+ const stream = new ReadableStream({
+ start(controller) {
+ archive.on('data', (chunk) => controller.enqueue(chunk));
+ archive.on('end', () => controller.close());
+ archive.on('error', (err) => {
+ console.error('Archive error:', err);
+ controller.error(err);
+ });
+ },
+ });
+
+ // 파일들을 ZIP에 추가 (폴더 구조 유지)
+ for (const { file, absolutePath, relativePath } of downloadableFiles) {
+ try {
+ const fileBuffer = await fs.readFile(absolutePath);
+ archive.append(fileBuffer, { name: relativePath });
+ } catch (error) {
+ console.error(`파일 추가 실패: ${relativePath}`, error);
+ }
+ }
+
+ // ZIP 완료
+ archive.finalize();
+
+ // Response Headers 설정
+ const headers = new Headers();
+ headers.set('Content-Type', 'application/zip');
+ headers.set('Content-Disposition', `attachment; filename="${encodeURIComponent(folder.name)}.zip"`);
+ headers.set('Cache-Control', 'no-cache, no-store, must-revalidate');
+ headers.set('Pragma', 'no-cache');
+ headers.set('Expires', '0');
+ headers.set('X-File-Count', downloadableFiles.length.toString());
+ headers.set('X-Total-Size', totalSize.toString());
+
+ return new NextResponse(stream, {
+ status: 200,
+ headers,
+ });
+
+ } catch (error) {
+ console.error('폴더 다운로드 오류:', error);
+ return NextResponse.json(
+ { error: '폴더 다운로드에 실패했습니다' },
+ { status: 500 }
+ );
+ }
+}
+
+// 폴더 다운로드 전 권한 체크 (선택적)
+export async function HEAD(
+ request: NextRequest,
+ { params }: { params: { projectId: string; folderId: string } }
+) {
+ try {
+ const session = await getServerSession(authOptions);
+ if (!session?.user) {
+ return new NextResponse(null, { status: 401 });
+ }
+
+ const context: FileAccessContext = {
+ userId: Number(session.user.id),
+ userDomain: session.user.domain || 'partners',
+ userEmail: session.user.email,
+ ipAddress: request.ip || request.headers.get('x-forwarded-for') || undefined,
+ userAgent: request.headers.get('user-agent') || undefined,
+ };
+
+ const fileService = new FileService();
+ let totalFiles = 0;
+ let unauthorizedCount = 0;
+ let totalSize = 0;
+
+ // 재귀적으로 권한 체크
+ const checkFolder = async (folderId: string): Promise<void> => {
+ const items = await db.query.fileItems.findMany({
+ where: and(
+ eq(fileItems.parentId, folderId),
+ eq(fileItems.projectId, params.projectId)
+ ),
+ });
+
+ for (const item of items) {
+ if (item.type === 'file') {
+ totalFiles++;
+ totalSize += item.size || 0;
+
+ const hasAccess = await fileService.checkFileAccess(
+ item.id,
+ context,
+ 'download'
+ );
+
+ if (!hasAccess) {
+ unauthorizedCount++;
+ }
+ } else if (item.type === 'folder') {
+ await checkFolder(item.id);
+ }
+ }
+ };
+
+ await checkFolder(params.folderId);
+
+ const headers = new Headers();
+ headers.set('X-Total-Files', totalFiles.toString());
+ headers.set('X-Unauthorized-Files', unauthorizedCount.toString());
+ headers.set('X-Total-Size', totalSize.toString());
+ headers.set('X-Can-Download', unauthorizedCount === 0 ? 'true' : 'false');
+
+ return new NextResponse(null, {
+ status: unauthorizedCount > 0 ? 403 : 200,
+ headers,
+ });
+
+ } catch (error) {
+ console.error('권한 체크 오류:', error);
+ return new NextResponse(null, { status: 500 });
+ }
+} \ No newline at end of file
diff --git a/app/api/data-room/[projectId]/download-multiple/route.ts b/app/api/data-room/[projectId]/download-multiple/route.ts
new file mode 100644
index 00000000..64c87b55
--- /dev/null
+++ b/app/api/data-room/[projectId]/download-multiple/route.ts
@@ -0,0 +1,162 @@
+// app/api/data-room/[projectId]/download-multiple/route.ts
+import { NextRequest, NextResponse } from 'next/server';
+import { getServerSession } from 'next-auth/next';
+import { authOptions } from '@/app/api/auth/[...nextauth]/route';
+import { FileService, type FileAccessContext } from '@/lib/services/fileService';
+import { promises as fs } from 'fs';
+import path from 'path';
+import archiver from 'archiver';
+import { Readable } from 'stream';
+import db from "@/db/db";
+import { fileItems } from "@/db/schema/fileSystem";
+import { eq, inArray } from "drizzle-orm";
+
+export async function POST(
+ request: NextRequest,
+ { params }: { params: { projectId: string } }
+) {
+ try {
+ const session = await getServerSession(authOptions);
+ if (!session?.user) {
+ return NextResponse.json({ error: '인증이 필요합니다' }, { status: 401 });
+ }
+
+ const body = await request.json();
+ const { fileIds } = body;
+
+ if (!fileIds || !Array.isArray(fileIds) || fileIds.length === 0) {
+ return NextResponse.json(
+ { error: '파일 ID가 제공되지 않았습니다' },
+ { status: 400 }
+ );
+ }
+
+ // 너무 많은 파일 방지 (최대 100개)
+ if (fileIds.length > 100) {
+ return NextResponse.json(
+ { error: '한 번에 최대 100개의 파일만 다운로드할 수 있습니다' },
+ { status: 400 }
+ );
+ }
+
+ const context: FileAccessContext = {
+ userId: Number(session.user.id),
+ userDomain: session.user.domain || 'partners',
+ userEmail: session.user.email,
+ ipAddress: request.ip || request.headers.get('x-forwarded-for') || undefined,
+ userAgent: request.headers.get('user-agent') || undefined,
+ };
+
+ const fileService = new FileService();
+ const downloadableFiles: Array<{
+ file: any;
+ absolutePath: string;
+ }> = [];
+
+ // 각 파일의 접근 권한 확인 및 경로 확인
+ for (const fileId of fileIds) {
+ // 권한 확인
+ const hasAccess = await fileService.checkFileAccess(
+ fileId,
+ context,
+ 'download'
+ );
+
+ if (!hasAccess) {
+ console.warn(`파일 ${fileId}에 대한 다운로드 권한이 없습니다`);
+ continue;
+ }
+
+ // 파일 정보 가져오기
+ const file = await db.query.fileItems.findFirst({
+ where: eq(fileItems.id, fileId),
+ });
+
+ if (!file || !file.filePath || file.type !== 'file') {
+ console.warn(`파일 ${fileId}를 찾을 수 없거나 폴더입니다`);
+ continue;
+ }
+
+ // 실제 파일 경로 구성
+ const nasPath = process.env.NAS_PATH || "/evcp_nas";
+ const isProduction = process.env.NODE_ENV === "production";
+
+ let absolutePath: string;
+ if (isProduction) {
+ const relativePath = file.filePath.replace('/api/files/', '');
+ absolutePath = path.join(nasPath, relativePath);
+ } else {
+ absolutePath = path.join(process.cwd(), 'public', file.filePath);
+ }
+
+ // 파일 존재 여부 확인
+ try {
+ await fs.access(absolutePath);
+ downloadableFiles.push({ file, absolutePath });
+
+ // 다운로드 카운트 증가 및 로그 기록
+ await fileService.downloadFile(fileId, context);
+ } catch (error) {
+ console.warn(`파일 ${absolutePath}를 찾을 수 없습니다`);
+ }
+ }
+
+ if (downloadableFiles.length === 0) {
+ return NextResponse.json(
+ { error: '다운로드 가능한 파일이 없습니다' },
+ { status: 404 }
+ );
+ }
+
+ // ZIP 스트림 생성
+ const archive = archiver('zip', {
+ zlib: { level: 5 } // 압축 레벨 (1-9, 5가 균형적)
+ });
+
+ // 스트림을 Response로 변환
+ const stream = new ReadableStream({
+ start(controller) {
+ archive.on('data', (chunk) => controller.enqueue(chunk));
+ archive.on('end', () => controller.close());
+ archive.on('error', (err) => controller.error(err));
+ },
+ });
+
+ // 파일들을 ZIP에 추가
+ for (const { file, absolutePath } of downloadableFiles) {
+ try {
+ const fileBuffer = await fs.readFile(absolutePath);
+
+ // 파일명 중복 방지를 위한 고유 이름 생성
+ const uniqueName = `${path.parse(file.name).name}_${file.id.slice(0, 8)}${path.extname(file.name)}`;
+
+ archive.append(fileBuffer, { name: uniqueName });
+ } catch (error) {
+ console.error(`파일 추가 실패: ${file.name}`, error);
+ }
+ }
+
+ // ZIP 완료
+ archive.finalize();
+
+ // Response Headers 설정
+ const headers = new Headers();
+ headers.set('Content-Type', 'application/zip');
+ headers.set('Content-Disposition', 'attachment; filename="files.zip"');
+ headers.set('Cache-Control', 'no-cache, no-store, must-revalidate');
+ headers.set('Pragma', 'no-cache');
+ headers.set('Expires', '0');
+
+ return new NextResponse(stream, {
+ status: 200,
+ headers,
+ });
+
+ } catch (error) {
+ console.error('다중 파일 다운로드 오류:', error);
+ return NextResponse.json(
+ { error: '다중 파일 다운로드에 실패했습니다' },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/data-room/[projectId]/permissions/route.ts b/app/api/data-room/[projectId]/permissions/route.ts
new file mode 100644
index 00000000..94401826
--- /dev/null
+++ b/app/api/data-room/[projectId]/permissions/route.ts
@@ -0,0 +1,74 @@
+// app/api/files/[projectId]/permissions/route.ts
+import { NextRequest, NextResponse } from 'next/server';
+import { getServerSession } from 'next-auth/next';
+import { authOptions } from '@/app/api/auth/[...nextauth]/route'
+import { FileService, type FileAccessContext } from '@/lib/services/fileService';
+import { z } from 'zod';
+
+const grantPermissionSchema = z.object({
+ fileId: z.string().uuid(),
+ targetUserId: z.number().optional().nullable(),
+ targetDomain: z.string().optional().nullable(),
+ permissions: z.object({
+ canView: z.boolean().optional(),
+ canDownload: z.boolean().optional(),
+ canEdit: z.boolean().optional(),
+ canDelete: z.boolean().optional(),
+ canShare: z.boolean().optional(),
+ }),
+});
+
+// 권한 부여
+export async function POST(
+ request: NextRequest,
+ { params }: { params: { projectId: string } }
+) {
+ try {
+ const session = await getServerSession(authOptions);
+ if (!session?.user) {
+ return NextResponse.json({ error: '인증이 필요합니다' }, { status: 401 });
+ }
+
+ const body = await request.json();
+ const validatedData = grantPermissionSchema.parse(body);
+
+ const context: FileAccessContext = {
+ userId: session.user.id,
+ userDomain: session.user.domain || 'partners',
+ userEmail: session.user.email,
+ ipAddress: request.ip || request.headers.get('x-forwarded-for') || undefined,
+ userAgent: request.headers.get('user-agent') || undefined,
+ };
+
+ const fileService = new FileService();
+ await fileService.grantPermission(
+ validatedData.fileId,
+ validatedData.targetUserId,
+ validatedData.targetDomain,
+ validatedData.permissions,
+ context
+ );
+
+ return NextResponse.json({ success: true });
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ return NextResponse.json(
+ { error: '잘못된 요청 데이터', details: error.errors },
+ { status: 400 }
+ );
+ }
+
+ if (error instanceof Error && error.message.includes('권한')) {
+ return NextResponse.json(
+ { error: error.message },
+ { status: 403 }
+ );
+ }
+
+ console.error('권한 부여 오류:', error);
+ return NextResponse.json(
+ { error: '권한 부여에 실패했습니다' },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/data-room/[projectId]/route.ts b/app/api/data-room/[projectId]/route.ts
new file mode 100644
index 00000000..643dcf0f
--- /dev/null
+++ b/app/api/data-room/[projectId]/route.ts
@@ -0,0 +1,118 @@
+// app/api/data-room/[projectId]/route.ts
+import { NextRequest, NextResponse } from 'next/server';
+import { getServerSession } from 'next-auth/next';
+import { authOptions } from '@/app/api/auth/[...nextauth]/route'
+import { FileService, type FileAccessContext } from '@/lib/services/fileService';
+import { z } from 'zod';
+
+// 파일 생성 스키마 검증
+const createFileSchema = z.object({
+ name: z.string().min(1).max(255),
+ type: z.enum(['file', 'folder']),
+ parentId: z.string().uuid().optional().nullable(),
+ category: z.enum(['public', 'restricted', 'confidential', 'internal']).default('confidential'),
+ mimeType: z.string().optional(),
+ size: z.number().optional(),
+ filePath: z.string().optional(),
+});
+
+// 파일 목록 조회
+export async function GET(
+ request: NextRequest,
+ { params }: { params: { projectId: string } }
+) {
+ try {
+ const session = await getServerSession(authOptions);
+ if (!session?.user) {
+ return NextResponse.json({ error: '인증이 필요합니다' }, { status: 401 });
+ }
+
+ const searchParams = request.nextUrl.searchParams;
+ const parentId = searchParams.get('parentId');
+ const viewMode = searchParams.get('viewMode'); // 'tree' or 'grid'
+ const includeAll = searchParams.get('includeAll') === 'true'; // 전체 목록 가져오기
+
+ const context: FileAccessContext = {
+ userId: Number(session.user.id),
+ userDomain: session.user.domain || 'partners',
+ userEmail: session.user.email,
+ ipAddress: request.ip || request.headers.get('x-forwarded-for') || undefined,
+ userAgent: request.headers.get('user-agent') || undefined,
+ };
+
+ const fileService = new FileService();
+
+ // viewMode가 tree이거나 includeAll이 true인 경우 전체 목록 가져오기
+ const files = await fileService.getFileList(
+ params.projectId,
+ parentId,
+ context,
+ {
+ includeAll: viewMode === 'tree' || includeAll
+ }
+ );
+
+ return NextResponse.json(files);
+ } catch (error) {
+ console.error('파일 목록 조회 오류:', error);
+ return NextResponse.json(
+ { error: '파일 목록을 불러올 수 없습니다' },
+ { status: 500 }
+ );
+ }
+}
+
+// 파일/폴더 생성
+export async function POST(
+ request: NextRequest,
+ { params }: { params: { projectId: string } }
+) {
+ try {
+ const session = await getServerSession(authOptions);
+ if (!session?.user) {
+ return NextResponse.json({ error: '인증이 필요합니다' }, { status: 401 });
+ }
+
+ const body = await request.json();
+ const validatedData = createFileSchema.parse(body);
+
+ const context: FileAccessContext = {
+ userId: Number(session.user.id),
+ userDomain: session.user.domain || 'partners',
+ userEmail: session.user.email,
+ ipAddress: request.ip || request.headers.get('x-forwarded-for') || undefined,
+ userAgent: request.headers.get('user-agent') || undefined,
+ };
+
+ const fileService = new FileService();
+ const newFile = await fileService.createFileItem(
+ {
+ ...validatedData,
+ projectId: params.projectId,
+ },
+ context
+ );
+
+ return NextResponse.json(newFile, { status: 201 });
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ return NextResponse.json(
+ { error: '잘못된 요청 데이터', details: error.errors },
+ { status: 400 }
+ );
+ }
+
+ if (error instanceof Error && error.message === '권한이 없습니다') {
+ return NextResponse.json(
+ { error: '파일 생성 권한이 없습니다' },
+ { status: 403 }
+ );
+ }
+
+ console.error('파일 생성 오류:', error);
+ return NextResponse.json(
+ { error: '파일 생성에 실패했습니다' },
+ { status: 500 }
+ );
+ }
+} \ No newline at end of file
diff --git a/app/api/data-room/[projectId]/share/[token]/route.ts b/app/api/data-room/[projectId]/share/[token]/route.ts
new file mode 100644
index 00000000..51582bca
--- /dev/null
+++ b/app/api/data-room/[projectId]/share/[token]/route.ts
@@ -0,0 +1,45 @@
+// app/api/shared/[token]/route.ts
+import { NextRequest, NextResponse } from 'next/server';
+import { FileService } from '@/lib/services/fileService';
+
+// 공유 링크로 파일 접근
+export async function GET(
+ request: NextRequest,
+ { params }: { params: { token: string } }
+) {
+ try {
+ const searchParams = request.nextUrl.searchParams;
+ const password = searchParams.get('password');
+
+ const fileService = new FileService();
+ const result = await fileService.accessFileByShareToken(
+ params.token,
+ password || undefined
+ );
+
+ if (!result) {
+ return NextResponse.json(
+ { error: '유효하지 않은 공유 링크입니다' },
+ { status: 404 }
+ );
+ }
+
+ return NextResponse.json({
+ file: result.file,
+ accessLevel: result.accessLevel,
+ });
+ } catch (error) {
+ if (error instanceof Error) {
+ return NextResponse.json(
+ { error: error.message },
+ { status: 400 }
+ );
+ }
+
+ console.error('공유 파일 접근 오류:', error);
+ return NextResponse.json(
+ { error: '파일 접근에 실패했습니다' },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/data-room/[projectId]/share/route.ts b/app/api/data-room/[projectId]/share/route.ts
new file mode 100644
index 00000000..9b27d9fc
--- /dev/null
+++ b/app/api/data-room/[projectId]/share/route.ts
@@ -0,0 +1,79 @@
+// app/api/files/[projectId]/share/route.ts
+import { NextRequest, NextResponse } from 'next/server';
+import { getServerSession } from 'next-auth/next';
+import { authOptions } from '@/app/api/auth/[...nextauth]/route'
+import { FileService, type FileAccessContext } from '@/lib/services/fileService';
+import { z } from 'zod';
+
+const createShareSchema = z.object({
+ fileId: z.string().uuid(),
+ accessLevel: z.enum(['view_only', 'view_download']).optional(),
+ password: z.string().optional(),
+ expiresAt: z.string().datetime().optional(),
+ maxDownloads: z.number().positive().optional(),
+ sharedWithEmail: z.string().email().optional(),
+});
+
+// 공유 링크 생성
+export async function POST(
+ request: NextRequest,
+ { params }: { params: { projectId: string } }
+) {
+ try {
+ const session = await getServerSession(authOptions);
+ if (!session?.user) {
+ return NextResponse.json({ error: '인증이 필요합니다' }, { status: 401 });
+ }
+
+ const body = await request.json();
+ const validatedData = createShareSchema.parse(body);
+
+ const context: FileAccessContext = {
+ userId: session.user.id,
+ userDomain: session.user.domain || 'partners',
+ userEmail: session.user.email,
+ ipAddress: request.ip || request.headers.get('x-forwarded-for') || undefined,
+ userAgent: request.headers.get('user-agent') || undefined,
+ };
+
+ const fileService = new FileService();
+ const shareToken = await fileService.createShareLink(
+ validatedData.fileId,
+ {
+ ...validatedData,
+ expiresAt: validatedData.expiresAt
+ ? new Date(validatedData.expiresAt)
+ : undefined,
+ },
+ context
+ );
+
+ const shareUrl = `${process.env.NEXT_PUBLIC_APP_URL}/shared/${shareToken}`;
+
+ return NextResponse.json({
+ shareToken,
+ shareUrl,
+ success: true
+ });
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ return NextResponse.json(
+ { error: '잘못된 요청 데이터', details: error.errors },
+ { status: 400 }
+ );
+ }
+
+ if (error instanceof Error && error.message.includes('권한')) {
+ return NextResponse.json(
+ { error: error.message },
+ { status: 403 }
+ );
+ }
+
+ console.error('공유 링크 생성 오류:', error);
+ return NextResponse.json(
+ { error: '공유 링크 생성에 실패했습니다' },
+ { status: 500 }
+ );
+ }
+} \ No newline at end of file
diff --git a/app/api/data-room/[projectId]/upload/route.ts b/app/api/data-room/[projectId]/upload/route.ts
new file mode 100644
index 00000000..60bbc10f
--- /dev/null
+++ b/app/api/data-room/[projectId]/upload/route.ts
@@ -0,0 +1,139 @@
+// app/api/data-room/[projectId]/upload/route.ts
+import { NextRequest, NextResponse } from 'next/server';
+import { getServerSession } from 'next-auth/next';
+import { authOptions } from '@/app/api/auth/[...nextauth]/route';
+import { FileService, type FileAccessContext } from '@/lib/services/fileService';
+import { saveDRMFile, saveFileStream } from '@/lib/file-stroage';
+
+export async function POST(
+ request: NextRequest,
+ { params }: { params: { projectId: string } }
+) {
+ try {
+ const session = await getServerSession(authOptions);
+ if (!session?.user) {
+ return NextResponse.json({ error: '인증이 필요합니다' }, { status: 401 });
+ }
+
+ const context: FileAccessContext = {
+ userId: Number(session.user.id),
+ userDomain: session.user.domain || 'partners',
+ userEmail: session.user.email,
+ ipAddress: request.ip || request.headers.get('x-forwarded-for') || undefined,
+ userAgent: request.headers.get('user-agent') || undefined,
+ };
+
+ // 내부 사용자만 업로드 가능
+ if (session.user.domain === 'partners') {
+ return NextResponse.json(
+ { error: '파일 업로드 권한이 없습니다' },
+ { status: 403 }
+ );
+ }
+
+ const formData = await request.formData();
+ const file = formData.get('file') as File;
+ const category = formData.get('category') as string;
+ const parentId = formData.get('parentId') as string | null;
+ const fileSize = formData.get('fileSize') as string | null;
+
+ if (!file) {
+ return NextResponse.json(
+ { error: '파일이 제공되지 않았습니다' },
+ { status: 400 }
+ );
+ }
+
+ // 대용량 파일 임계값 (10MB)
+ const LARGE_FILE_THRESHOLD = 10 * 1024 * 1024;
+ const actualFileSize = fileSize ? parseInt(fileSize) : file.size;
+
+ let result;
+
+ // 파일 크기에 따라 다른 저장 방법 사용
+ if (actualFileSize > LARGE_FILE_THRESHOLD) {
+ console.log(`🚀 대용량 파일 스트리밍 저장: ${file.name} (${(actualFileSize / 1024 / 1024).toFixed(2)}MB)`);
+
+ // 대용량 파일은 스트리밍 저장 사용
+ result = await saveFileStream({
+ file,
+ directory: `projects/${params.projectId}`,
+ originalName: file.name,
+ userId: session.user.id
+ });
+ } else {
+ console.log(`📦 일반 파일 저장: ${file.name} (${(actualFileSize / 1024 / 1024).toFixed(2)}MB)`);
+
+ // 작은 파일은 기존 DRM 저장 방식 사용
+ result = await saveDRMFile(
+ file,
+ async (file) => file.arrayBuffer(), // 이미 복호화된 데이터
+ `projects/${params.projectId}`,
+ session.user.id
+ );
+ }
+
+ if (!result.success) {
+ return NextResponse.json(
+ { error: result.error || '파일 저장에 실패했습니다' },
+ { status: 500 }
+ );
+ }
+
+ // DB에 파일 정보 저장
+ const fileService = new FileService();
+ const newFile = await fileService.createFileItem(
+ {
+ name: result.originalName || file.name,
+ type: 'file',
+ category: category as 'public' | 'restricted' | 'confidential' | 'internal',
+ parentId,
+ size: result.fileSize || actualFileSize,
+ mimeType: file.type,
+ filePath: result.publicPath,
+ projectId: params.projectId,
+ },
+ context
+ );
+
+ return NextResponse.json({
+ ...newFile,
+ uploadResult: {
+ ...result,
+ uploadMethod: actualFileSize > LARGE_FILE_THRESHOLD ? 'stream' : 'buffer',
+ fileSizeMB: (actualFileSize / 1024 / 1024).toFixed(2)
+ },
+ }, { status: 201 });
+
+ } catch (error) {
+ console.error('파일 업로드 오류:', error);
+ return NextResponse.json(
+ {
+ error: '파일 업로드에 실패했습니다',
+ details: error instanceof Error ? error.message : undefined
+ },
+ { status: 500 }
+ );
+ }
+}
+
+// 업로드 진행률 확인 (선택적)
+export async function GET(
+ request: NextRequest,
+ { params }: { params: { projectId: string } }
+) {
+ try {
+ const session = await getServerSession(authOptions);
+ if (!session?.user) {
+ return NextResponse.json({ error: '인증이 필요합니다' }, { status: 401 });
+ }
+
+ // 업로드 상태 확인 로직 (필요시 구현)
+ return NextResponse.json({ message: '업로드 상태 확인 엔드포인트' });
+ } catch (error) {
+ return NextResponse.json(
+ { error: '상태 확인 실패' },
+ { status: 500 }
+ );
+ }
+} \ No newline at end of file