ERP-node/backend-node/src/utils/fileSystemManager.ts

327 lines
8.8 KiB
TypeScript

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<void> {
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 회사 코드
* @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 => {
const items = fs.readdirSync(dirPath);
for (const item of items) {
const itemPath = path.join(dirPath, item);
const stats = fs.statSync(itemPath);
if (stats.isFile() && item === serverFilename) {
return itemPath;
} else if (stats.isDirectory()) {
const found = findFileRecursively(itemPath);
if (found) return found;
}
}
return null;
};
return findFileRecursively(companyBasePath);
} catch (error) {
logger.error(`파일 검색 실패: ${companyCode}/${serverFilename}`, error);
return null;
}
}
/**
* 회사별 디스크 사용량 조회
* @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;
}
}
}