import { logger } from "../utils/logger"; import { query } from "../database/db"; // 환경 변수로 데이터 소스 선택 const DATA_SOURCE = process.env.DOCUMENT_DATA_SOURCE || "memory"; export interface Document { id: string; name: string; category: "계약서" | "보험" | "세금계산서" | "기타"; fileSize: number; filePath: string; mimeType?: string; uploadDate: string; description?: string; uploadedBy?: string; relatedEntityType?: string; relatedEntityId?: string; tags?: string[]; isArchived: boolean; archivedAt?: string; } // 메모리 목 데이터 const mockDocuments: Document[] = [ { id: "doc-1", name: "2025년 1월 세금계산서.pdf", category: "세금계산서", fileSize: 1258291, filePath: "/uploads/documents/tax-invoice-202501.pdf", uploadDate: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(), description: "1월 매출 세금계산서", uploadedBy: "admin", isArchived: false, }, { id: "doc-2", name: "차량보험증권_서울12가3456.pdf", category: "보험", fileSize: 876544, filePath: "/uploads/documents/insurance-vehicle-1.pdf", uploadDate: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000).toISOString(), description: "1톤 트럭 종합보험", uploadedBy: "admin", isArchived: false, }, { id: "doc-3", name: "운송계약서_ABC물류.pdf", category: "계약서", fileSize: 2457600, filePath: "/uploads/documents/contract-abc-logistics.pdf", uploadDate: new Date(Date.now() - 15 * 24 * 60 * 60 * 1000).toISOString(), description: "ABC물류 연간 운송 계약", uploadedBy: "admin", isArchived: false, }, { id: "doc-4", name: "2024년 12월 세금계산서.pdf", category: "세금계산서", fileSize: 1124353, filePath: "/uploads/documents/tax-invoice-202412.pdf", uploadDate: new Date(Date.now() - 40 * 24 * 60 * 60 * 1000).toISOString(), uploadedBy: "admin", isArchived: false, }, { id: "doc-5", name: "화물배상책임보험증권.pdf", category: "보험", fileSize: 720384, filePath: "/uploads/documents/cargo-insurance.pdf", uploadDate: new Date(Date.now() - 50 * 24 * 60 * 60 * 1000).toISOString(), description: "화물 배상책임보험", uploadedBy: "admin", isArchived: false, }, { id: "doc-6", name: "차고지 임대계약서.pdf", category: "계약서", fileSize: 1843200, filePath: "/uploads/documents/garage-lease-contract.pdf", uploadDate: new Date(Date.now() - 60 * 24 * 60 * 60 * 1000).toISOString(), uploadedBy: "admin", isArchived: false, }, ]; /** * 문서 관리 서비스 (Memory/DB 하이브리드) */ export class DocumentService { private static instance: DocumentService; private constructor() { logger.info(`📂 문서 관리 데이터 소스: ${DATA_SOURCE.toUpperCase()}`); } public static getInstance(): DocumentService { if (!DocumentService.instance) { DocumentService.instance = new DocumentService(); } return DocumentService.instance; } public async getAllDocuments(filter?: { category?: string; searchTerm?: string; uploadedBy?: string; }): Promise { try { const documents = DATA_SOURCE === "database" ? await this.loadDocumentsFromDB(filter) : this.loadDocumentsFromMemory(filter); // 최신순 정렬 documents.sort((a, b) => new Date(b.uploadDate).getTime() - new Date(a.uploadDate).getTime() ); return documents; } catch (error) { logger.error("❌ 문서 목록 조회 오류:", error); throw error; } } public async getDocumentById(id: string): Promise { try { if (DATA_SOURCE === "database") { return await this.getDocumentByIdDB(id); } else { return this.getDocumentByIdMemory(id); } } catch (error) { logger.error("❌ 문서 조회 오류:", error); throw error; } } public async getStatistics(): Promise<{ total: number; byCategory: Record; totalSize: number; }> { try { const documents = await this.getAllDocuments(); const byCategory: Record = { "계약서": 0, "보험": 0, "세금계산서": 0, "기타": 0, }; documents.forEach((doc) => { byCategory[doc.category] = (byCategory[doc.category] || 0) + 1; }); const totalSize = documents.reduce((sum, doc) => sum + doc.fileSize, 0); return { total: documents.length, byCategory, totalSize, }; } catch (error) { logger.error("❌ 문서 통계 조회 오류:", error); throw error; } } // ==================== DATABASE 메서드 ==================== private async loadDocumentsFromDB(filter?: { category?: string; searchTerm?: string; uploadedBy?: string; }): Promise { let sql = ` SELECT id, name, category, file_size as "fileSize", file_path as "filePath", mime_type as "mimeType", upload_date as "uploadDate", description, uploaded_by as "uploadedBy", related_entity_type as "relatedEntityType", related_entity_id as "relatedEntityId", tags, is_archived as "isArchived", archived_at as "archivedAt" FROM document_files WHERE is_archived = false `; const params: any[] = []; let paramIndex = 1; if (filter?.category) { sql += ` AND category = $${paramIndex++}`; params.push(filter.category); } if (filter?.searchTerm) { sql += ` AND (name ILIKE $${paramIndex} OR description ILIKE $${paramIndex})`; params.push(`%${filter.searchTerm}%`); paramIndex++; } if (filter?.uploadedBy) { sql += ` AND uploaded_by = $${paramIndex++}`; params.push(filter.uploadedBy); } const rows = await query(sql, params); return rows.map((row: any) => ({ ...row, uploadDate: new Date(row.uploadDate).toISOString(), archivedAt: row.archivedAt ? new Date(row.archivedAt).toISOString() : undefined, })); } private async getDocumentByIdDB(id: string): Promise { const rows = await query( `SELECT id, name, category, file_size as "fileSize", file_path as "filePath", mime_type as "mimeType", upload_date as "uploadDate", description, uploaded_by as "uploadedBy", related_entity_type as "relatedEntityType", related_entity_id as "relatedEntityId", tags, is_archived as "isArchived", archived_at as "archivedAt" FROM document_files WHERE id = $1`, [id] ); if (rows.length === 0) { throw new Error(`문서를 찾을 수 없습니다: ${id}`); } const row = rows[0]; return { ...row, uploadDate: new Date(row.uploadDate).toISOString(), archivedAt: row.archivedAt ? new Date(row.archivedAt).toISOString() : undefined, }; } // ==================== MEMORY 메서드 ==================== private loadDocumentsFromMemory(filter?: { category?: string; searchTerm?: string; uploadedBy?: string; }): Document[] { let documents = mockDocuments.filter((d) => !d.isArchived); if (filter?.category) { documents = documents.filter((d) => d.category === filter.category); } if (filter?.searchTerm) { const term = filter.searchTerm.toLowerCase(); documents = documents.filter( (d) => d.name.toLowerCase().includes(term) || d.description?.toLowerCase().includes(term) ); } if (filter?.uploadedBy) { documents = documents.filter((d) => d.uploadedBy === filter.uploadedBy); } return documents; } private getDocumentByIdMemory(id: string): Document { const document = mockDocuments.find((d) => d.id === id); if (!document) { throw new Error(`문서를 찾을 수 없습니다: ${id}`); } return document; } }