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]/route.ts4
-rw-r--r--app/api/data-room/[projectId]/files/route.ts246
-rw-r--r--app/api/data-room/[projectId]/folders/route.ts282
3 files changed, 530 insertions, 2 deletions
diff --git a/app/api/data-room/[projectId]/[fileId]/route.ts b/app/api/data-room/[projectId]/[fileId]/route.ts
index 9ee01eb2..5e6d5088 100644
--- a/app/api/data-room/[projectId]/[fileId]/route.ts
+++ b/app/api/data-room/[projectId]/[fileId]/route.ts
@@ -21,7 +21,7 @@ export async function GET(
}
const context: FileAccessContext = {
- userId: session.user.id,
+ userId: Number(session.user.id),
userDomain: session.user.domain || 'partners',
userEmail: session.user.email,
ipAddress: request.ip || request.headers.get('x-forwarded-for') || undefined,
@@ -201,7 +201,7 @@ export async function PATCH(
projectId,
action,
actionDetails,
- userId: session.user.id,
+ userId: Number(session.user.id),
userEmail: session.user.email,
userDomain: session.user.domain,
});
diff --git a/app/api/data-room/[projectId]/files/route.ts b/app/api/data-room/[projectId]/files/route.ts
new file mode 100644
index 00000000..8c99c77f
--- /dev/null
+++ b/app/api/data-room/[projectId]/files/route.ts
@@ -0,0 +1,246 @@
+// app/api/data-room/[projectId]/files/route.ts
+import { NextRequest, NextResponse } from 'next/server';
+import { getServerSession } from 'next-auth/next';
+import { authOptions } from '@/app/api/auth/[...nextauth]/route';
+import { fileItems } from '@/db/schema';
+import { and, eq, isNull, desc, asc, sql } from 'drizzle-orm';
+import db from '@/db/db';
+
+// 파일 목록 조회
+export async function GET(
+ request: NextRequest,
+ { params }: { params: Promise<{ projectId: string }> }
+) {
+ try {
+ const { projectId } = await params;
+
+ // 세션 확인
+ const session = await getServerSession(authOptions);
+ if (!session?.user) {
+ return NextResponse.json({ error: '인증이 필요합니다' }, { status: 401 });
+ }
+
+ // URL 파라미터 파싱
+ const { searchParams } = new URL(request.url);
+ const parentId = searchParams.get('parentId');
+ const category = searchParams.get('category');
+ const type = searchParams.get('type'); // 'file' | 'folder' | 'all'
+ const sortBy = searchParams.get('sortBy') || 'name';
+ const sortOrder = searchParams.get('sortOrder') || 'asc';
+
+ // 기본 조회 조건 설정
+ const conditions = [];
+
+ // 프로젝트 ID는 필수
+ conditions.push(eq(fileItems.projectId, projectId));
+
+ // parentId 조건 추가
+ if (parentId && parentId !== 'null') {
+ conditions.push(eq(fileItems.parentId, parentId));
+ } else {
+ // parentId가 없으면 최상위 항목만 조회
+ conditions.push(isNull(fileItems.parentId));
+ }
+
+ // 카테고리 필터
+ if (category) {
+ conditions.push(eq(fileItems.category, category));
+ }
+
+ // 타입 필터 (file, folder, all)
+ if (type && type !== 'all') {
+ if (type === 'file' || type === 'folder') {
+ conditions.push(eq(fileItems.type, type));
+ }
+ }
+
+ // 파일 목록 조회
+ const files = await db
+ .select()
+ .from(fileItems)
+ .where(conditions.length > 0 ? and(...conditions) : undefined)
+ .orderBy(
+ // 폴더를 먼저 표시
+ desc(fileItems.type),
+ // 그 다음 정렬 기준 적용
+ sortBy === 'name'
+ ? (sortOrder === 'asc' ? asc(fileItems.name) : desc(fileItems.name))
+ : sortBy === 'updatedAt'
+ ? (sortOrder === 'asc' ? asc(fileItems.updatedAt) : desc(fileItems.updatedAt))
+ : sortBy === 'size'
+ ? (sortOrder === 'asc' ? asc(fileItems.size) : desc(fileItems.size))
+ : asc(fileItems.name) // 기본값
+ );
+
+ // 파트너사 사용자의 경우 접근 가능한 파일만 필터링
+ let filteredFiles = files;
+ if (session.user.domain === 'partners') {
+ // 현재는 모든 파일을 볼 수 있도록 설정
+ // 필요시 추가 필터링 로직 구현
+ filteredFiles = files;
+ }
+
+ // 응답 데이터 구성
+ const response = {
+ files: filteredFiles.map(file => ({
+ id: file.id,
+ projectId: file.projectId,
+ parentId: file.parentId,
+ name: file.name,
+ type: file.type,
+ path: file.path || '/',
+ category: file.category || 'uncategorized',
+ size: file.size || 0,
+ mimeType: file.mimeType || '',
+ uploadedBy: file.uploadedBy,
+ uploadedByDomain: file.uploadedByDomain || 'default',
+ createdAt: file.createdAt,
+ updatedAt: file.updatedAt,
+ // 내부 사용자에게만 추가 정보 제공
+ ...(session.user.domain !== 'partners' && {
+ createdBy: file.createdBy,
+ updatedBy: file.updatedBy,
+ }),
+ })),
+ count: filteredFiles.length,
+ parentId: parentId || null,
+ // 현재 경로 정보 (브레드크럼용)
+ currentPath: parentId ? await getCurrentPath(parentId, projectId) : [],
+ };
+
+ return NextResponse.json(response);
+
+ } catch (error) {
+ console.error('파일 목록 조회 오류:', error);
+ return NextResponse.json(
+ {
+ error: '파일 목록을 불러오는데 실패했습니다',
+ details: process.env.NODE_ENV === 'development' ? error.message : undefined
+ },
+ { status: 500 }
+ );
+ }
+}
+
+// 현재 경로 정보 가져오기 (브레드크럼용)
+async function getCurrentPath(
+ folderId: string,
+ projectId: string
+): Promise<Array<{ id: string; name: string }>> {
+ try {
+ const path: Array<{ id: string; name: string }> = [];
+ let currentId: string | null = folderId;
+ let depth = 0;
+ const maxDepth = 10;
+
+ while (currentId && depth < maxDepth) {
+ const result = await db
+ .select({
+ id: fileItems.id,
+ name: fileItems.name,
+ parentId: fileItems.parentId,
+ })
+ .from(fileItems)
+ .where(
+ and(
+ eq(fileItems.id, currentId),
+ eq(fileItems.projectId, projectId),
+ eq(fileItems.type, 'folder')
+ )
+ )
+ .limit(1);
+
+ const folder = result[0];
+ if (!folder) break;
+
+ // 경로 앞에 추가 (역순이므로)
+ path.unshift({ id: folder.id, name: folder.name });
+ currentId = folder.parentId;
+ depth++;
+ }
+
+ return path;
+ } catch (error) {
+ console.error('경로 조회 오류:', error);
+ return [];
+ }
+}
+
+// 파일 검색 (POST)
+export async function POST(
+ request: NextRequest,
+ { params }: { params: Promise<{ projectId: string }> }
+) {
+ try {
+ const { projectId } = await params;
+
+ const session = await getServerSession(authOptions);
+ if (!session?.user) {
+ return NextResponse.json({ error: '인증이 필요합니다' }, { status: 401 });
+ }
+
+ const body = await request.json();
+ const { searchTerm, filters } = body;
+
+ if (!searchTerm || searchTerm.trim().length < 2) {
+ return NextResponse.json({
+ error: '검색어는 2글자 이상 입력해주세요'
+ }, { status: 400 });
+ }
+
+ // 검색 쿼리 생성
+ const searchConditions = [
+ eq(fileItems.projectId, projectId),
+ sql`LOWER(${fileItems.name}) LIKE LOWER(${'%' + searchTerm + '%'})`
+ ];
+
+ // 필터 적용
+ if (filters?.category) {
+ searchConditions.push(eq(fileItems.category, filters.category));
+ }
+ if (filters?.type) {
+ searchConditions.push(eq(fileItems.type, filters.type));
+ }
+
+ const searchResults = await db
+ .select()
+ .from(fileItems)
+ .where(and(...searchConditions))
+ .orderBy(desc(fileItems.type), asc(fileItems.name))
+ .limit(50); // 최대 50개 결과
+
+ // 파트너사 권한 필터링
+ let filteredResults = searchResults;
+ if (session.user.domain === 'partners') {
+ // 현재는 모든 결과 반환
+ filteredResults = searchResults;
+ }
+
+ return NextResponse.json({
+ results: filteredResults.map(file => ({
+ id: file.id,
+ projectId: file.projectId,
+ parentId: file.parentId,
+ name: file.name,
+ type: file.type,
+ path: file.path || '/',
+ category: file.category || 'uncategorized',
+ size: file.size || 0,
+ mimeType: file.mimeType || '',
+ updatedAt: file.updatedAt,
+ })),
+ count: filteredResults.length,
+ searchTerm,
+ });
+
+ } catch (error) {
+ console.error('파일 검색 오류:', error);
+ return NextResponse.json(
+ {
+ error: '파일 검색에 실패했습니다',
+ details: process.env.NODE_ENV === 'development' ? error.message : undefined
+ },
+ { status: 500 }
+ );
+ }
+} \ No newline at end of file
diff --git a/app/api/data-room/[projectId]/folders/route.ts b/app/api/data-room/[projectId]/folders/route.ts
new file mode 100644
index 00000000..0ddf48f5
--- /dev/null
+++ b/app/api/data-room/[projectId]/folders/route.ts
@@ -0,0 +1,282 @@
+// app/api/data-room/[projectId]/folders/route.ts
+import { NextRequest, NextResponse } from 'next/server';
+import { getServerSession } from 'next-auth/next';
+import { authOptions } from '@/app/api/auth/[...nextauth]/route';
+import { fileItems, fileActivityLogs } from '@/db/schema';
+import { and, eq } from 'drizzle-orm';
+import db from '@/db/db';
+
+// 폴더 생성
+export async function POST(
+ request: NextRequest,
+ { params }: { params: Promise<{ projectId: string }> }
+) {
+ try {
+ const { projectId } = await params;
+
+ // 세션 확인
+ const session = await getServerSession(authOptions);
+ if (!session?.user) {
+ return NextResponse.json({ error: 'Authentication required' }, { status: 401 });
+ }
+
+ // 내부 사용자만 폴더 생성 가능
+ if (session.user.domain === 'partners') {
+ return NextResponse.json({ error: 'Permission denied' }, { status: 403 });
+ }
+
+ // 요청 본문 파싱
+ const body = await request.json();
+ const { name, parentId, category } = body;
+
+ // 필수 필드 검증
+ if (!name || typeof name !== 'string' || !name.trim()) {
+ return NextResponse.json({ error: 'Folder name is required' }, { status: 400 });
+ }
+
+ // 폴더 이름 정리
+ const folderName = name.trim();
+
+ // 폴더 이름 유효성 검사
+ const invalidChars = /[<>:"|?*]/;
+ if (invalidChars.test(folderName)) {
+ return NextResponse.json({
+ error: 'Folder name contains invalid characters'
+ }, { status: 400 });
+ }
+
+ // 부모 폴더 정보 조회 및 경로 설정
+ let parentFolder = null;
+ let path = '/';
+ let depth = 0;
+ let inheritedCategory = category || 'confidential'; // 기본값을 스키마에 맞게 변경
+
+ if (parentId && parentId !== null) {
+ const parentFolderResult = await db
+ .select()
+ .from(fileItems)
+ .where(
+ and(
+ eq(fileItems.id, parentId),
+ eq(fileItems.projectId, projectId),
+ eq(fileItems.type, 'folder')
+ )
+ )
+ .limit(1);
+
+ if (parentFolderResult.length === 0) {
+ return NextResponse.json({ error: 'Parent folder not found' }, { status: 404 });
+ }
+
+ parentFolder = parentFolderResult[0];
+ // 경로 계산 (부모 경로 + 부모 이름 + /)
+ path = parentFolder.path + parentFolder.name + '/';
+ // depth는 부모 depth + 1
+ depth = (parentFolder.depth || 0) + 1;
+
+ // 카테고리가 지정되지 않았으면 부모 폴더의 카테고리 상속
+ if (!category) {
+ inheritedCategory = parentFolder.category || 'confidential';
+ }
+ }
+
+ // 같은 위치에 중복 이름 확인
+ const existingFolder = await db
+ .select()
+ .from(fileItems)
+ .where(
+ and(
+ eq(fileItems.projectId, projectId),
+ eq(fileItems.parentId, parentId || null),
+ eq(fileItems.name, folderName),
+ eq(fileItems.type, 'folder')
+ )
+ )
+ .limit(1);
+
+ if (existingFolder.length > 0) {
+ return NextResponse.json({
+ error: 'A folder with this name already exists'
+ }, { status: 409 });
+ }
+
+ // 새 폴더 생성 (UUID는 자동 생성됨)
+ const newFolder = {
+ projectId,
+ parentId: parentId || null,
+ name: folderName,
+ type: 'folder' as const,
+ path,
+ depth,
+ category: inheritedCategory,
+ size: 0,
+ mimeType: 'folder',
+ // filePath와 fileUrl은 폴더에서는 null
+ filePath: null,
+ fileUrl: null,
+ // 권한 설정
+ externalAccessLevel: 'view_only' as const,
+ externalAccessExpiry: null,
+ downloadCount: 0,
+ viewCount: 0,
+ // 메타데이터
+ metadata: {},
+ tags: null,
+ // 버전 관리
+ version: 1,
+ previousVersionId: null,
+ // 감사 로그
+ createdBy: Number(session.user.id),
+ updatedBy: Number(session.user.id),
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ };
+
+ // DB에 폴더 저장
+ const [insertedFolder] = await db
+ .insert(fileItems)
+ .values(newFolder)
+ .returning();
+
+ // 활동 로그 기록 (스키마가 있다면)
+ if (fileActivityLogs) {
+ try {
+ await db.insert(fileActivityLogs).values({
+ fileItemId: insertedFolder.id,
+ projectId,
+ action: 'create_folder',
+ actionDetails: {
+ folderName: folderName,
+ parentId: parentId || null,
+ parentName: parentFolder?.name || null,
+ path,
+ category: inheritedCategory,
+ depth,
+ },
+ userId: Number(session.user.id),
+ userEmail: session.user.email,
+ userDomain: session.user.domain || 'default',
+ ipAddress: request.ip || request.headers.get('x-forwarded-for') || null,
+ userAgent: request.headers.get('user-agent') || null,
+ createdAt: new Date(),
+ });
+ } catch (logError) {
+ // 로그 실패는 무시하고 계속 진행
+ console.error('Failed to create activity log:', logError);
+ }
+ }
+
+ console.log('Folder created successfully:', {
+ id: insertedFolder.id,
+ name: folderName,
+ path,
+ depth,
+ parentId: parentId || null,
+ });
+
+ // 성공 응답
+ return NextResponse.json({
+ success: true,
+ folder: insertedFolder,
+ message: 'Folder created successfully',
+ }, { status: 201 });
+
+ } catch (error) {
+ console.error('Folder creation error:', error);
+
+ // 에러 타입에 따른 응답
+ if (error instanceof Error) {
+ // PostgreSQL unique constraint violation
+ if (error.message.includes('unique') || error.message.includes('duplicate')) {
+ return NextResponse.json(
+ { error: 'A folder with this path already exists' },
+ { status: 409 }
+ );
+ }
+
+ // Foreign key constraint violation
+ if (error.message.includes('foreign key')) {
+ return NextResponse.json(
+ { error: 'Invalid project or parent folder' },
+ { status: 400 }
+ );
+ }
+
+ // 데이터베이스 연결 오류
+ if (error.message.includes('connect')) {
+ return NextResponse.json(
+ { error: 'Database connection failed' },
+ { status: 503 }
+ );
+ }
+ }
+
+ // 일반 오류
+ return NextResponse.json(
+ {
+ error: 'Failed to create folder',
+ details: process.env.NODE_ENV === 'development' ? error?.message : undefined
+ },
+ { status: 500 }
+ );
+ }
+}
+
+// 폴더 목록 조회
+export async function GET(
+ request: NextRequest,
+ { params }: { params: Promise<{ projectId: string }> }
+) {
+ try {
+ const { projectId } = await params;
+
+ const session = await getServerSession(authOptions);
+ if (!session?.user) {
+ return NextResponse.json({ error: 'Authentication required' }, { status: 401 });
+ }
+
+ // URL 파라미터에서 parentId 가져오기
+ const { searchParams } = new URL(request.url);
+ const parentId = searchParams.get('parentId');
+
+ // 폴더만 조회
+ const conditions = [
+ eq(fileItems.projectId, projectId),
+ eq(fileItems.type, 'folder'),
+ ];
+
+ if (parentId) {
+ conditions.push(eq(fileItems.parentId, parentId));
+ } else {
+ conditions.push(eq(fileItems.parentId, null));
+ }
+
+ const folders = await db
+ .select()
+ .from(fileItems)
+ .where(and(...conditions))
+ .orderBy(fileItems.name);
+
+ // 파트너사 사용자의 경우 접근 가능한 폴더만 필터링
+ let filteredFolders = folders;
+ if (session.user.domain === 'partners') {
+ // category가 'confidential'이 아닌 폴더만 표시
+ filteredFolders = folders.filter(folder =>
+ folder.category !== 'confidential' ||
+ folder.externalAccessLevel !== null
+ );
+ }
+
+ return NextResponse.json({
+ folders: filteredFolders,
+ count: filteredFolders.length,
+ });
+
+ } catch (error) {
+ console.error('Folder list error:', error);
+ return NextResponse.json(
+ { error: 'Failed to load folders' },
+ { status: 500 }
+ );
+ }
+} \ No newline at end of file