283 lines
7.9 KiB
TypeScript
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;
|
|
}
|
|
}
|
|
|