ERP-node/backend-node/src/services/documentService.ts

283 lines
7.9 KiB
TypeScript

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<Document[]> {
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<Document> {
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<string, number>;
totalSize: number;
}> {
try {
const documents = await this.getAllDocuments();
const byCategory: Record<string, number> = {
"계약서": 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<Document[]> {
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<Document> {
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;
}
}