파일 업로드,다운로드 기능

This commit is contained in:
kjs
2025-09-05 12:04:13 +09:00
parent aa066a1ea9
commit 53a44b901d
10 changed files with 1028 additions and 91 deletions

View File

@@ -35,7 +35,7 @@
"@types/jest": "^29.5.11",
"@types/jsonwebtoken": "^9.0.5",
"@types/morgan": "^1.9.9",
"@types/multer": "^1.4.11",
"@types/multer": "^1.4.13",
"@types/node": "^20.10.5",
"@types/node-cron": "^3.0.11",
"@types/nodemailer": "^6.4.14",

View File

@@ -53,7 +53,7 @@
"@types/jest": "^29.5.11",
"@types/jsonwebtoken": "^9.0.5",
"@types/morgan": "^1.9.9",
"@types/multer": "^1.4.11",
"@types/multer": "^1.4.13",
"@types/node": "^20.10.5",
"@types/node-cron": "^3.0.11",
"@types/nodemailer": "^6.4.14",

View File

@@ -16,6 +16,7 @@ import tableManagementRoutes from "./routes/tableManagementRoutes";
import screenManagementRoutes from "./routes/screenManagementRoutes";
import commonCodeRoutes from "./routes/commonCodeRoutes";
import dynamicFormRoutes from "./routes/dynamicFormRoutes";
import fileRoutes from "./routes/fileRoutes";
// import userRoutes from './routes/userRoutes';
// import menuRoutes from './routes/menuRoutes';
@@ -79,6 +80,7 @@ app.use("/api/table-management", tableManagementRoutes);
app.use("/api/screen-management", screenManagementRoutes);
app.use("/api/common-codes", commonCodeRoutes);
app.use("/api/dynamic-form", dynamicFormRoutes);
app.use("/api/files", fileRoutes);
// app.use('/api/users', userRoutes);
// app.use('/api/menus', menuRoutes);

View File

@@ -0,0 +1,306 @@
import express from "express";
import multer from "multer";
import path from "path";
import fs from "fs";
import { authenticateToken } from "../middleware/authMiddleware";
import { AuthenticatedRequest } from "../types/auth";
import { logger } from "../utils/logger";
const router = express.Router();
// 파일 저장 경로 설정
const UPLOAD_PATH = path.join(process.cwd(), "uploads");
// uploads 디렉토리가 없으면 생성
if (!fs.existsSync(UPLOAD_PATH)) {
fs.mkdirSync(UPLOAD_PATH, { recursive: true });
}
// Multer 설정 - 파일 업로드용
const storage = multer.diskStorage({
destination: (req, file, cb) => {
return cb(null, UPLOAD_PATH);
},
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);
},
});
const upload = multer({
storage,
limits: {
fileSize: 50 * 1024 * 1024, // 50MB 제한
},
fileFilter: (req, file, cb) => {
// 허용된 파일 타입 검사 (필요시 확장)
const allowedTypes = [
"image/jpeg",
"image/png",
"image/gif",
"image/webp",
"application/pdf",
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"text/plain",
"text/csv",
];
if (allowedTypes.includes(file.mimetype)) {
return cb(null, true);
} else {
return cb(new Error(`허용되지 않는 파일 타입입니다: ${file.mimetype}`));
}
},
});
// 모든 라우트에 인증 미들웨어 적용
router.use(authenticateToken);
/**
* 파일 업로드
* POST /api/files/upload
*/
router.post(
"/upload",
upload.array("files", 10),
async (req: AuthenticatedRequest, res): Promise<void> => {
try {
const files = req.files as Express.Multer.File[];
if (!files || files.length === 0) {
res.status(400).json({
success: false,
message: "업로드할 파일이 없습니다.",
});
return;
}
const fileInfos = files.map((file) => ({
id: `file_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
name: Buffer.from(file.originalname, "latin1").toString("utf8"),
size: file.size,
type: file.mimetype,
extension: path.extname(file.originalname).toLowerCase().substring(1),
uploadedAt: new Date().toISOString(),
lastModified: new Date().toISOString(),
serverPath: file.path,
serverFilename: file.filename,
}));
logger.info("파일 업로드 완료", {
userId: req.user?.userId,
fileCount: files.length,
files: fileInfos.map((f) => ({ name: f.name, size: f.size })),
});
res.json({
success: true,
message: `${files.length}개 파일이 성공적으로 업로드되었습니다.`,
files: fileInfos,
});
} catch (error) {
logger.error("파일 업로드 오류:", error);
res.status(500).json({
success: false,
message: "파일 업로드 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류",
});
}
}
);
/**
* 파일 다운로드
* GET /api/files/download/:fileId
*/
router.get(
"/download/:fileId",
async (req: AuthenticatedRequest, res): Promise<void> => {
try {
const { fileId } = req.params;
const { serverFilename, originalName } = req.query;
if (!serverFilename || !originalName) {
res.status(400).json({
success: false,
message:
"파일 정보가 부족합니다. (serverFilename, originalName 필요)",
});
return;
}
const filePath = path.join(UPLOAD_PATH, serverFilename as string);
// 파일 존재 확인
if (!fs.existsSync(filePath)) {
logger.warn("파일을 찾을 수 없음", {
fileId,
serverFilename,
filePath,
userId: req.user?.userId,
});
res.status(404).json({
success: false,
message: "요청한 파일을 찾을 수 없습니다.",
});
return;
}
// 파일 정보 확인
const stats = fs.statSync(filePath);
logger.info("파일 다운로드 요청", {
fileId,
originalName,
serverFilename,
fileSize: stats.size,
userId: req.user?.userId,
});
// 파일명 인코딩 (한글 파일명 지원)
const encodedFilename = encodeURIComponent(originalName as string);
// 응답 헤더 설정
res.setHeader(
"Content-Disposition",
`attachment; filename*=UTF-8''${encodedFilename}`
);
res.setHeader("Content-Type", "application/octet-stream");
res.setHeader("Content-Length", stats.size);
res.setHeader("Cache-Control", "no-cache");
// 파일 스트림으로 전송
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 : "알 수 없는 오류",
});
}
}
);
/**
* 파일 삭제
* DELETE /api/files/:fileId
*/
router.delete(
"/:fileId",
async (req: AuthenticatedRequest, res): Promise<void> => {
try {
const { fileId } = req.params;
const { serverFilename } = req.body;
if (!serverFilename) {
res.status(400).json({
success: false,
message: "서버 파일명이 필요합니다.",
});
return;
}
const filePath = path.join(UPLOAD_PATH, serverFilename);
// 파일 존재 확인
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
logger.info("파일 삭제 완료", {
fileId,
serverFilename,
userId: req.user?.userId,
});
}
res.json({
success: true,
message: "파일이 성공적으로 삭제되었습니다.",
});
} catch (error) {
logger.error("파일 삭제 오류:", error);
res.status(500).json({
success: false,
message: "파일 삭제 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류",
});
}
}
);
/**
* 파일 정보 조회
* GET /api/files/info/:fileId
*/
router.get(
"/info/: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 filePath = path.join(UPLOAD_PATH, serverFilename as string);
if (!fs.existsSync(filePath)) {
res.status(404).json({
success: false,
message: "파일을 찾을 수 없습니다.",
});
return;
}
const stats = fs.statSync(filePath);
res.json({
success: true,
data: {
fileId,
serverFilename,
size: stats.size,
lastModified: stats.mtime.toISOString(),
exists: true,
},
});
} catch (error) {
logger.error("파일 정보 조회 오류:", error);
res.status(500).json({
success: false,
message: "파일 정보 조회 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류",
});
}
}
);
export default router;

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB