281 lines
7.5 KiB
TypeScript
281 lines
7.5 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 회사 코드
|
||
|
|
*/
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|