파일 업로드,다운로드 기능
This commit is contained in:
2
backend-node/package-lock.json
generated
2
backend-node/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
306
backend-node/src/routes/fileRoutes.ts
Normal file
306
backend-node/src/routes/fileRoutes.ts
Normal 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;
|
||||
BIN
backend-node/uploads/1757041371158_IMG_0343.jpeg
Normal file
BIN
backend-node/uploads/1757041371158_IMG_0343.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.7 MiB |
Reference in New Issue
Block a user