import fs from "fs"; import path from "path"; import { logger } from "./logger"; /** * 파일 시스템 관리 유틸리티 * 회사별 폴더 구조 관리 */ export class FileSystemManager { private static readonly BASE_UPLOAD_PATH = path.join( process.cwd(), "uploads" ); private static readonly SHARED_FOLDER = "shared"; /** * 회사별 업로드 경로 생성 * @param companyCode 회사 코드 * @param date 업로드 날짜 (선택적) * @returns 생성된 폴더 경로 */ static createCompanyUploadPath(companyCode: string, date?: Date): string { const uploadDate = date || new Date(); const year = uploadDate.getFullYear(); const month = String(uploadDate.getMonth() + 1).padStart(2, "0"); const day = String(uploadDate.getDate()).padStart(2, "0"); const folderPath = path.join( this.BASE_UPLOAD_PATH, `company_${companyCode}`, String(year), month, day ); // 폴더가 없으면 생성 (recursive) if (!fs.existsSync(folderPath)) { fs.mkdirSync(folderPath, { recursive: true }); logger.info(`회사별 업로드 폴더 생성: ${folderPath}`); } return folderPath; } /** * 회사 등록 시 기본 폴더 구조 생성 * @param companyCode 회사 코드 */ static initializeCompanyFolder(companyCode: string): void { try { const companyBasePath = path.join( this.BASE_UPLOAD_PATH, `company_${companyCode}` ); if (!fs.existsSync(companyBasePath)) { fs.mkdirSync(companyBasePath, { recursive: true }); // README 파일 생성 (폴더 설명) const readmePath = path.join(companyBasePath, "README.txt"); const readmeContent = `회사 코드: ${companyCode} 생성일: ${new Date().toISOString()} 폴더 구조: YYYY/MM/DD/파일명 관리자: 시스템 자동 생성`; fs.writeFileSync(readmePath, readmeContent, "utf8"); logger.info(`회사 폴더 초기화 완료: ${companyCode}`); } } catch (error) { logger.error(`회사 폴더 초기화 실패: ${companyCode}`, error); throw error; } } /** * 회사 삭제 시 폴더 및 파일 정리 * @param companyCode 회사 코드 * @param createBackup 백업 생성 여부 */ static async cleanupCompanyFiles( companyCode: string, createBackup: boolean = true ): Promise { try { const companyPath = path.join( this.BASE_UPLOAD_PATH, `company_${companyCode}` ); if (!fs.existsSync(companyPath)) { logger.warn(`삭제할 회사 폴더가 없습니다: ${companyCode}`); return; } // 백업 생성 if (createBackup) { const backupPath = path.join( this.BASE_UPLOAD_PATH, "deleted_companies", `${companyCode}_${Date.now()}` ); if (!fs.existsSync(path.dirname(backupPath))) { fs.mkdirSync(path.dirname(backupPath), { recursive: true }); } // 폴더 이동 (백업) fs.renameSync(companyPath, backupPath); logger.info(`회사 파일 백업 완료: ${companyCode} -> ${backupPath}`); } else { // 완전 삭제 fs.rmSync(companyPath, { recursive: true, force: true }); logger.info(`회사 파일 완전 삭제: ${companyCode}`); } } catch (error) { logger.error(`회사 파일 정리 실패: ${companyCode}`, error); throw error; } } /** * 공통 파일 경로 (시스템 관리용) */ static getSharedPath(): string { const sharedPath = path.join(this.BASE_UPLOAD_PATH, this.SHARED_FOLDER); if (!fs.existsSync(sharedPath)) { fs.mkdirSync(sharedPath, { recursive: true }); } return sharedPath; } /** * 회사별 디스크 사용량 조회 * @param companyCode 회사 코드 */ static getCompanyDiskUsage(companyCode: string): { fileCount: number; totalSize: number; } { try { const companyPath = path.join( this.BASE_UPLOAD_PATH, `company_${companyCode}` ); if (!fs.existsSync(companyPath)) { return { fileCount: 0, totalSize: 0 }; } let fileCount = 0; let totalSize = 0; const scanDirectory = (dirPath: string) => { const items = fs.readdirSync(dirPath); for (const item of items) { const itemPath = path.join(dirPath, item); const stats = fs.statSync(itemPath); if (stats.isDirectory()) { scanDirectory(itemPath); } else { fileCount++; totalSize += stats.size; } } }; scanDirectory(companyPath); return { fileCount, totalSize }; } catch (error) { logger.error(`디스크 사용량 조회 실패: ${companyCode}`, error); return { fileCount: 0, totalSize: 0 }; } } /** * 전체 시스템 디스크 사용량 조회 */ static getAllCompaniesDiskUsage(): Array<{ companyCode: string; fileCount: number; totalSize: number; }> { try { const baseDir = this.BASE_UPLOAD_PATH; if (!fs.existsSync(baseDir)) { return []; } const companies = fs .readdirSync(baseDir) .filter((item) => item.startsWith("company_")) .map((folder) => folder.replace("company_", "")); return companies.map((companyCode) => ({ companyCode, ...this.getCompanyDiskUsage(companyCode), })); } catch (error) { logger.error("전체 디스크 사용량 조회 실패", error); return []; } } /** * 안전한 파일명 생성 (기존 로직 + 회사 정보) * @param originalName 원본 파일명 * @param companyCode 회사 코드 */ static generateSafeFileName( originalName: string, companyCode: string ): string { const timestamp = Date.now(); const cleanName = Buffer.from(originalName, "latin1").toString("utf8"); const ext = path.extname(cleanName); const nameWithoutExt = path.basename(cleanName, ext); // 회사코드_타임스탬프_파일명 형식 return `${companyCode}_${timestamp}_${nameWithoutExt}${ext}`; } /** * 회사별 폴더에서 파일 찾기 * @param companyCode 회사 코드 * @param serverFilename 서버 파일명 * @returns 파일 경로 (찾지 못하면 null) */ static findFileInCompanyFolders( companyCode: string, serverFilename: string ): string | null { try { const companyBasePath = path.join( this.BASE_UPLOAD_PATH, `company_${companyCode}` ); if (!fs.existsSync(companyBasePath)) { return null; } // 재귀적으로 폴더 탐색 const findFileRecursively = (dirPath: string): string | null => { try { const items = fs.readdirSync(dirPath); for (const item of items) { const itemPath = path.join(dirPath, item); const stats = fs.statSync(itemPath); if (stats.isDirectory()) { // 하위 폴더에서 재귀 검색 const found = findFileRecursively(itemPath); if (found) return found; } else if (item === serverFilename) { // 파일을 찾았으면 경로 반환 return itemPath; } } } catch (error) { logger.error(`폴더 탐색 오류: ${dirPath}`, error); } return null; }; return findFileRecursively(companyBasePath); } catch (error) { logger.error(`파일 찾기 실패: ${companyCode}/${serverFilename}`, error); return null; } } }