diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-10-29 07:46:57 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-10-29 07:46:57 +0000 |
| commit | bbc3094e932e3d193d3223448c789461f4afc058 (patch) | |
| tree | f28dd034b191ca78d0af15eccbbdf7a952141153 /app/api | |
| parent | d28c43b2d33bac51c69ac7417a14f9fe83f2a25f (diff) | |
(대표님) 데이터룸 관련 변경사항
Diffstat (limited to 'app/api')
| -rw-r--r-- | app/api/data-room/[projectId]/[fileId]/route.ts | 4 | ||||
| -rw-r--r-- | app/api/data-room/[projectId]/files/route.ts | 246 | ||||
| -rw-r--r-- | app/api/data-room/[projectId]/folders/route.ts | 282 |
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 |
