이미지 미리보기 기능

This commit is contained in:
kjs
2025-09-05 14:52:10 +09:00
parent dcc459850c
commit 20cdcca171
16 changed files with 1093 additions and 48 deletions

View File

@@ -0,0 +1,182 @@
import express from "express";
import { authenticateToken } from "../middleware/authMiddleware";
import { AuthenticatedRequest } from "../types/auth";
import { logger } from "../utils/logger";
import { FileSystemManager } from "../utils/fileSystemManager";
import { PrismaClient } from "@prisma/client";
const router = express.Router();
const prisma = new PrismaClient();
// 모든 라우트에 인증 미들웨어 적용
router.use(authenticateToken);
/**
* DELETE /api/company-management/:companyCode
* 회사 삭제 및 파일 정리
*/
router.delete(
"/:companyCode",
async (req: AuthenticatedRequest, res): Promise<void> => {
try {
const { companyCode } = req.params;
const { createBackup = true } = req.body;
logger.info("회사 삭제 요청", {
companyCode,
createBackup,
userId: req.user?.userId,
});
// 1. 회사 존재 확인
const existingCompany = await prisma.company_mng.findUnique({
where: { company_code: companyCode },
});
if (!existingCompany) {
res.status(404).json({
success: false,
message: "존재하지 않는 회사입니다.",
errorCode: "COMPANY_NOT_FOUND",
});
return;
}
// 2. 회사 파일 정리 (백업 또는 삭제)
try {
await FileSystemManager.cleanupCompanyFiles(companyCode, createBackup);
logger.info("회사 파일 정리 완료", { companyCode, createBackup });
} catch (fileError) {
logger.error("회사 파일 정리 실패", { companyCode, error: fileError });
res.status(500).json({
success: false,
message: "회사 파일 정리 중 오류가 발생했습니다.",
error:
fileError instanceof Error ? fileError.message : "Unknown error",
});
return;
}
// 3. 데이터베이스에서 회사 삭제 (soft delete)
await prisma.company_mng.update({
where: { company_code: companyCode },
data: {
status: "deleted",
},
});
logger.info("회사 삭제 완료", {
companyCode,
companyName: existingCompany.company_name,
deletedBy: req.user?.userId,
});
res.json({
success: true,
message: `회사 '${existingCompany.company_name}'이(가) 성공적으로 삭제되었습니다.`,
data: {
companyCode,
companyName: existingCompany.company_name,
backupCreated: createBackup,
deletedAt: new Date().toISOString(),
},
});
} catch (error) {
logger.error("회사 삭제 실패", {
error,
companyCode: req.params.companyCode,
});
res.status(500).json({
success: false,
message: "회사 삭제 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "Unknown error",
});
}
}
);
/**
* GET /api/company-management/:companyCode/disk-usage
* 회사별 디스크 사용량 조회
*/
router.get(
"/:companyCode/disk-usage",
async (req: AuthenticatedRequest, res): Promise<void> => {
try {
const { companyCode } = req.params;
const diskUsage = FileSystemManager.getCompanyDiskUsage(companyCode);
res.json({
success: true,
data: {
companyCode,
fileCount: diskUsage.fileCount,
totalSize: diskUsage.totalSize,
totalSizeMB:
Math.round((diskUsage.totalSize / 1024 / 1024) * 100) / 100,
lastChecked: new Date().toISOString(),
},
});
} catch (error) {
logger.error("디스크 사용량 조회 실패", {
error,
companyCode: req.params.companyCode,
});
res.status(500).json({
success: false,
message: "디스크 사용량 조회 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "Unknown error",
});
}
}
);
/**
* GET /api/company-management/disk-usage/all
* 전체 회사 디스크 사용량 조회
*/
router.get(
"/disk-usage/all",
async (req: AuthenticatedRequest, res): Promise<void> => {
try {
const allUsage = FileSystemManager.getAllCompaniesDiskUsage();
const totalStats = allUsage.reduce(
(acc, company) => ({
totalFiles: acc.totalFiles + company.fileCount,
totalSize: acc.totalSize + company.totalSize,
}),
{ totalFiles: 0, totalSize: 0 }
);
res.json({
success: true,
data: {
companies: allUsage.map((company) => ({
...company,
totalSizeMB:
Math.round((company.totalSize / 1024 / 1024) * 100) / 100,
})),
summary: {
totalCompanies: allUsage.length,
totalFiles: totalStats.totalFiles,
totalSize: totalStats.totalSize,
totalSizeMB:
Math.round((totalStats.totalSize / 1024 / 1024) * 100) / 100,
},
lastChecked: new Date().toISOString(),
},
});
} catch (error) {
logger.error("전체 디스크 사용량 조회 실패", { error });
res.status(500).json({
success: false,
message: "전체 디스크 사용량 조회 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "Unknown error",
});
}
}
);
export default router;

View File

@@ -5,6 +5,7 @@ import fs from "fs";
import { authenticateToken } from "../middleware/authMiddleware";
import { AuthenticatedRequest } from "../types/auth";
import { logger } from "../utils/logger";
import { FileSystemManager } from "../utils/fileSystemManager";
const router = express.Router();
@@ -16,21 +17,53 @@ if (!fs.existsSync(UPLOAD_PATH)) {
fs.mkdirSync(UPLOAD_PATH, { recursive: true });
}
// Multer 설정 - 파일 업로드용
// Multer 설정 - 회사별 폴더 구조 지원
const storage = multer.diskStorage({
destination: (req, file, cb) => {
return cb(null, UPLOAD_PATH);
try {
// 사용자의 회사 코드 가져오기
const user = (req as AuthenticatedRequest).user;
const companyCode = user?.companyCode || "default";
// 회사별 날짜별 폴더 생성
const uploadPath = FileSystemManager.createCompanyUploadPath(companyCode);
logger.info("파일 업로드 대상 폴더", {
companyCode,
uploadPath,
userId: user?.userId,
});
return cb(null, uploadPath);
} catch (error) {
logger.error("업로드 폴더 생성 실패", error);
return cb(error as Error, "");
}
},
filename: (req, file, cb) => {
// 파일명: timestamp_originalname
const timestamp = Date.now();
const originalName = Buffer.from(file.originalname, "latin1").toString(
"utf8"
);
const ext = path.extname(originalName);
const nameWithoutExt = path.basename(originalName, ext);
const safeFileName = `${timestamp}_${nameWithoutExt}${ext}`;
return cb(null, safeFileName);
try {
// 사용자의 회사 코드 가져오기
const user = (req as AuthenticatedRequest).user;
const companyCode = user?.companyCode || "default";
// 회사코드가 포함된 안전한 파일명 생성
const safeFileName = FileSystemManager.generateSafeFileName(
file.originalname,
companyCode
);
logger.info("파일명 생성", {
originalName: file.originalname,
safeFileName,
companyCode,
userId: user?.userId,
});
return cb(null, safeFileName);
} catch (error) {
logger.error("파일명 생성 실패", error);
return cb(error as Error, "");
}
},
});
@@ -251,6 +284,128 @@ router.delete(
}
);
/**
* 이미지 미리보기
* GET /api/files/preview/:fileId
*/
router.get(
"/preview/:fileId",
async (req: AuthenticatedRequest, res): Promise<void> => {
try {
const { fileId } = req.params;
const { serverFilename } = req.query;
if (!serverFilename) {
res.status(400).json({
success: false,
message: "서버 파일명이 필요합니다.",
});
return;
}
// 회사별 폴더 구조를 고려하여 파일 경로 찾기
const user = req.user;
const companyCode = user?.companyCode || "default";
// 먼저 회사별 폴더에서 찾기
let filePath = FileSystemManager.findFileInCompanyFolders(
companyCode,
serverFilename as string
);
// 찾지 못하면 기본 uploads 폴더에서 찾기
if (!filePath) {
filePath = path.join(UPLOAD_PATH, serverFilename as string);
}
// 파일 존재 확인
if (!fs.existsSync(filePath)) {
logger.warn("이미지 파일을 찾을 수 없음", {
fileId,
serverFilename,
filePath,
userId: user?.userId,
});
res.status(404).json({
success: false,
message: "요청한 이미지 파일을 찾을 수 없습니다.",
});
return;
}
// 파일 확장자로 MIME 타입 결정
const ext = path.extname(filePath).toLowerCase();
const mimeTypes: { [key: string]: string } = {
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
".webp": "image/webp",
".bmp": "image/bmp",
".svg": "image/svg+xml",
};
const mimeType = mimeTypes[ext] || "application/octet-stream";
// 이미지 파일이 아닌 경우 에러 반환
if (!mimeType.startsWith("image/")) {
res.status(400).json({
success: false,
message: "이미지 파일이 아닙니다.",
});
return;
}
// 파일 정보 확인
const stats = fs.statSync(filePath);
logger.info("이미지 미리보기 요청", {
fileId,
serverFilename,
mimeType,
fileSize: stats.size,
userId: user?.userId,
});
// 캐시 헤더 설정 (이미지는 캐시 가능)
res.setHeader("Content-Type", mimeType);
res.setHeader("Content-Length", stats.size);
res.setHeader("Cache-Control", "public, max-age=86400"); // 24시간 캐시
res.setHeader("Last-Modified", stats.mtime.toUTCString());
// If-Modified-Since 헤더 확인
const ifModifiedSince = req.headers["if-modified-since"];
if (ifModifiedSince && new Date(ifModifiedSince) >= stats.mtime) {
res.status(304).end();
return;
}
// 파일 스트림으로 전송
const fileStream = fs.createReadStream(filePath);
fileStream.on("error", (error) => {
logger.error("이미지 스트림 오류:", error);
if (!res.headersSent) {
res.status(500).json({
success: false,
message: "이미지 전송 중 오류가 발생했습니다.",
});
}
});
fileStream.pipe(res);
} catch (error) {
logger.error("이미지 미리보기 오류:", error);
res.status(500).json({
success: false,
message: "이미지 미리보기 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류",
});
}
}
);
/**
* 파일 정보 조회
* GET /api/files/info/:fileId