파일 컴포넌트 분리

This commit is contained in:
kjs
2025-09-05 21:52:19 +09:00
parent e0fd624078
commit d73be8a4d3
21 changed files with 1999 additions and 556 deletions

View File

@@ -181,20 +181,38 @@ export class AuthController {
return;
}
const userInfoResponse: UserInfo = {
// DB에서 조회한 원본 사용자 정보 로그
console.log("🔍 DB에서 조회한 사용자 정보:", {
userId: dbUserInfo.userId,
companyCode: dbUserInfo.companyCode,
deptCode: dbUserInfo.deptCode,
dbUserInfoKeys: Object.keys(dbUserInfo),
});
// 프론트엔드 호환성을 위해 더 많은 사용자 정보 반환
const userInfoResponse: any = {
userId: dbUserInfo.userId,
userName: dbUserInfo.userName || "",
deptName: dbUserInfo.deptName || "",
companyCode: dbUserInfo.companyCode || "ILSHIN",
company_code: dbUserInfo.companyCode || "ILSHIN", // 프론트엔드 호환성
userType: dbUserInfo.userType || "USER",
userTypeName: dbUserInfo.userTypeName || "일반사용자",
email: dbUserInfo.email || "",
photo: dbUserInfo.photo,
locale: dbUserInfo.locale || "KR", // locale 정보 추가
deptCode: dbUserInfo.deptCode, // 추가 필드
isAdmin:
dbUserInfo.userType === "ADMIN" || dbUserInfo.userId === "plm_admin",
};
console.log("📤 프론트엔드로 전송할 사용자 정보:", {
companyCode: userInfoResponse.companyCode,
company_code: userInfoResponse.company_code,
deptCode: userInfoResponse.deptCode,
responseKeys: Object.keys(userInfoResponse),
});
res.status(200).json({
success: true,
message: "사용자 정보 조회 성공",

View File

@@ -0,0 +1,444 @@
import { Request, Response } from "express";
import { AuthenticatedRequest } from "../types/auth";
import multer from "multer";
import path from "path";
import fs from "fs";
import { PrismaClient } from "@prisma/client";
import { generateUUID } from "../utils/generateId";
const prisma = new PrismaClient();
// 업로드 디렉토리 설정 (회사별로 분리)
const baseUploadDir = path.join(process.cwd(), "uploads");
if (!fs.existsSync(baseUploadDir)) {
fs.mkdirSync(baseUploadDir, { recursive: true });
}
// 회사별 + 날짜별 디렉토리 생성 함수
const getCompanyUploadDir = (companyCode: string, dateFolder?: string) => {
// 회사코드가 *인 경우 company_*로 변환
const actualCompanyCode = companyCode === "*" ? "company_*" : companyCode;
// 날짜 폴더가 제공되지 않은 경우 오늘 날짜 사용 (YYYY/MM/DD 형식)
if (!dateFolder) {
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, "0");
const day = String(today.getDate()).padStart(2, "0");
dateFolder = `${year}/${month}/${day}`;
}
const companyDir = path.join(baseUploadDir, actualCompanyCode, dateFolder);
if (!fs.existsSync(companyDir)) {
fs.mkdirSync(companyDir, { recursive: true });
}
return companyDir;
};
// Multer 설정
const storage = multer.diskStorage({
destination: (req, file, cb) => {
// 임시 디렉토리에 저장 (나중에 올바른 위치로 이동)
const tempDir = path.join(baseUploadDir, "temp");
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
console.log(`📁 임시 업로드 디렉토리: ${tempDir}`);
cb(null, tempDir);
},
filename: (req, file, cb) => {
// 타임스탬프_원본파일명 형태로 저장 (회사코드는 디렉토리로 분리됨)
const timestamp = Date.now();
const sanitizedName = file.originalname.replace(/[^a-zA-Z0-9.-]/g, "_");
const savedFileName = `${timestamp}_${sanitizedName}`;
console.log(`📄 저장 파일명: ${savedFileName}`);
cb(null, savedFileName);
},
});
const upload = multer({
storage: storage,
limits: {
fileSize: 50 * 1024 * 1024, // 50MB 제한
},
fileFilter: (req, file, cb) => {
// 파일 타입 검증
const allowedTypes = [
"image/jpeg",
"image/png",
"image/gif",
"application/pdf",
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error("허용되지 않는 파일 타입입니다."));
}
},
});
/**
* 파일 업로드 및 attach_file_info 테이블에 저장
*/
export const uploadFiles = async (
req: AuthenticatedRequest,
res: Response
): Promise<void> => {
try {
console.log("📤 파일 업로드 요청 수신:", {
body: req.body,
companyCode: req.body.companyCode,
writer: req.body.writer,
docType: req.body.docType,
user: req.user
? {
userId: req.user.userId,
companyCode: req.user.companyCode,
deptCode: req.user.deptCode,
}
: "no user",
files: req.files
? (req.files as Express.Multer.File[]).map((f) => f.originalname)
: "none",
});
if (!req.files || (req.files as Express.Multer.File[]).length === 0) {
res.status(400).json({
success: false,
message: "업로드할 파일이 없습니다.",
});
return;
}
const files = req.files as Express.Multer.File[];
const {
docType = "DOCUMENT",
docTypeName = "일반 문서",
targetObjid,
parentTargetObjid,
} = req.body;
// 회사코드와 작성자 정보 결정 (우선순위: 요청 body > 사용자 토큰 정보 > 기본값)
const companyCode =
req.body.companyCode || (req.user as any)?.companyCode || "DEFAULT";
const writer = req.body.writer || (req.user as any)?.userId || "system";
console.log("🔍 사용자 정보 결정:", {
bodyCompanyCode: req.body.companyCode,
userCompanyCode: (req.user as any)?.companyCode,
finalCompanyCode: companyCode,
bodyWriter: req.body.writer,
userWriter: (req.user as any)?.userId,
finalWriter: writer,
});
const savedFiles = [];
for (const file of files) {
// 파일 확장자 추출
const fileExt = path
.extname(file.originalname)
.toLowerCase()
.replace(".", "");
// 파일 경로 설정 (회사별 + 날짜별 디렉토리 구조 반영)
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, "0");
const day = String(today.getDate()).padStart(2, "0");
const dateFolder = `${year}/${month}/${day}`;
// 회사코드가 *인 경우 company_*로 변환
const actualCompanyCode = companyCode === "*" ? "company_*" : companyCode;
const relativePath = `/${actualCompanyCode}/${dateFolder}/${file.filename}`;
const fullFilePath = `/uploads${relativePath}`;
console.log("📂 파일 경로 설정:", {
companyCode,
filename: file.filename,
relativePath,
fullFilePath,
});
// 임시 파일을 최종 위치로 이동
const tempFilePath = file.path; // Multer가 저장한 임시 파일 경로
const finalUploadDir = getCompanyUploadDir(companyCode, dateFolder);
const finalFilePath = path.join(finalUploadDir, file.filename);
console.log("📦 파일 이동:", {
from: tempFilePath,
to: finalFilePath,
});
// 파일 이동
fs.renameSync(tempFilePath, finalFilePath);
// attach_file_info 테이블에 저장
const fileRecord = await prisma.attach_file_info.create({
data: {
objid: parseInt(
generateUUID().replace(/-/g, "").substring(0, 15),
16
),
target_objid: targetObjid,
saved_file_name: file.filename,
real_file_name: file.originalname,
doc_type: docType,
doc_type_name: docTypeName,
file_size: file.size,
file_ext: fileExt,
file_path: fullFilePath, // 회사별 디렉토리 포함된 경로
company_code: companyCode, // 회사코드 추가
writer: writer,
regdate: new Date(),
status: "ACTIVE",
parent_target_objid: parentTargetObjid,
},
});
console.log("💾 파일 정보 DB 저장 완료:", {
objid: fileRecord.objid.toString(),
saved_file_name: fileRecord.saved_file_name,
real_file_name: fileRecord.real_file_name,
file_size: fileRecord.file_size?.toString(),
});
savedFiles.push({
objid: fileRecord.objid.toString(),
savedFileName: fileRecord.saved_file_name,
realFileName: fileRecord.real_file_name,
fileSize: Number(fileRecord.file_size),
fileExt: fileRecord.file_ext,
filePath: fileRecord.file_path,
docType: fileRecord.doc_type,
docTypeName: fileRecord.doc_type_name,
targetObjid: fileRecord.target_objid,
parentTargetObjid: fileRecord.parent_target_objid,
companyCode: companyCode, // 실제 전달받은 회사코드
writer: fileRecord.writer,
regdate: fileRecord.regdate?.toISOString(),
status: fileRecord.status,
});
console.log("✅ 파일 저장 결과:", {
objid: fileRecord.objid.toString(),
company_code: companyCode,
file_path: fileRecord.file_path,
writer: fileRecord.writer,
});
}
res.json({
success: true,
message: `${files.length}개 파일 업로드 완료`,
files: savedFiles,
});
} catch (error) {
console.error("파일 업로드 오류:", error);
res.status(500).json({
success: false,
message: "파일 업로드 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류",
});
}
};
/**
* 파일 삭제 (논리적 삭제)
*/
export const deleteFile = async (
req: AuthenticatedRequest,
res: Response
): Promise<void> => {
try {
const { objid } = req.params;
const { writer = "system" } = req.body;
console.log("🗑️ 파일 삭제 요청:", { objid, writer });
// 파일 상태를 DELETED로 변경 (논리적 삭제)
const deletedFile = await prisma.attach_file_info.update({
where: {
objid: parseInt(objid),
},
data: {
status: "DELETED",
},
});
console.log("✅ 파일 삭제 완료 (논리적):", {
objid: deletedFile.objid.toString(),
status: deletedFile.status,
});
res.json({
success: true,
message: "파일이 삭제되었습니다.",
});
} catch (error) {
console.error("파일 삭제 오류:", error);
res.status(500).json({
success: false,
message: "파일 삭제 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류",
});
}
};
/**
* 파일 목록 조회
*/
export const getFileList = async (
req: AuthenticatedRequest,
res: Response
): Promise<void> => {
try {
const { targetObjid, docType, companyCode } = req.query;
console.log("📋 파일 목록 조회 요청:", {
targetObjid,
docType,
companyCode,
});
const where: any = {
status: "ACTIVE",
};
if (targetObjid) {
where.target_objid = targetObjid as string;
}
if (docType) {
where.doc_type = docType as string;
}
const files = await prisma.attach_file_info.findMany({
where,
orderBy: {
regdate: "desc",
},
});
const fileList = files.map((file: any) => ({
objid: file.objid.toString(),
savedFileName: file.saved_file_name,
realFileName: file.real_file_name,
fileSize: Number(file.file_size),
fileExt: file.file_ext,
filePath: file.file_path,
docType: file.doc_type,
docTypeName: file.doc_type_name,
targetObjid: file.target_objid,
parentTargetObjid: file.parent_target_objid,
writer: file.writer,
regdate: file.regdate?.toISOString(),
status: file.status,
}));
res.json({
success: true,
files: fileList,
});
} catch (error) {
console.error("파일 목록 조회 오류:", error);
res.status(500).json({
success: false,
message: "파일 목록 조회 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류",
});
}
};
/**
* 파일 다운로드
*/
export const downloadFile = async (
req: AuthenticatedRequest,
res: Response
): Promise<void> => {
try {
const { objid } = req.params;
console.log("📥 파일 다운로드 요청:", { objid });
const fileRecord = await prisma.attach_file_info.findUnique({
where: {
objid: parseInt(objid),
},
});
if (!fileRecord || fileRecord.status !== "ACTIVE") {
res.status(404).json({
success: false,
message: "파일을 찾을 수 없습니다.",
});
return;
}
// 파일 경로에서 회사코드와 날짜 폴더 추출 (예: /uploads/company_*/2025/09/05/timestamp_filename.ext)
const filePathParts = fileRecord.file_path!.split("/");
const companyCode = filePathParts[2] || "DEFAULT"; // /uploads/company_*/2025/09/05/filename.ext에서 company_* 추출
const fileName = fileRecord.saved_file_name!;
// 파일 경로에 날짜 구조가 있는지 확인 (YYYY/MM/DD)
let dateFolder = "";
if (filePathParts.length >= 6) {
// /uploads/company_*/2025/09/05/filename.ext 형태
dateFolder = `${filePathParts[3]}/${filePathParts[4]}/${filePathParts[5]}`;
}
const companyUploadDir = getCompanyUploadDir(
companyCode,
dateFolder || undefined
);
const filePath = path.join(companyUploadDir, fileName);
console.log("📥 파일 다운로드 경로 확인:", {
stored_file_path: fileRecord.file_path,
company_code: companyCode,
company_upload_dir: companyUploadDir,
final_file_path: filePath,
});
if (!fs.existsSync(filePath)) {
console.error("❌ 파일 없음:", filePath);
res.status(404).json({
success: false,
message: `실제 파일을 찾을 수 없습니다: ${filePath}`,
});
return;
}
// 파일 다운로드 헤더 설정
res.setHeader(
"Content-Disposition",
`attachment; filename="${encodeURIComponent(fileRecord.real_file_name!)}"`
);
res.setHeader("Content-Type", "application/octet-stream");
// 파일 스트림 전송
const fileStream = fs.createReadStream(filePath);
fileStream.pipe(res);
console.log("✅ 파일 다운로드 시작:", {
objid: fileRecord.objid.toString(),
real_file_name: fileRecord.real_file_name,
});
} catch (error) {
console.error("파일 다운로드 오류:", error);
res.status(500).json({
success: false,
message: "파일 다운로드 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류",
});
}
};
// Multer 미들웨어 export
export const uploadMiddleware = upload.array("files", 10); // 최대 10개 파일

View File

@@ -1,497 +1,45 @@
import express from "express";
import multer from "multer";
import path from "path";
import fs from "fs";
import { Router } from "express";
import {
uploadFiles,
deleteFile,
getFileList,
downloadFile,
uploadMiddleware,
} from "../controllers/fileController";
import { authenticateToken } from "../middleware/authMiddleware";
import { AuthenticatedRequest } from "../types/auth";
import { logger } from "../utils/logger";
import { FileSystemManager } from "../utils/fileSystemManager";
const router = express.Router();
const router = 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) => {
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) => {
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, "");
}
},
});
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}`));
}
},
});
// 모든 라우트에 인증 미들웨어 적용
// 모든 파일 API는 인증 필요
router.use(authenticateToken);
/**
* 파일 업로드
* POST /api/files/upload
* @route POST /api/files/upload
* @desc 파일 업로드 (attach_file_info 테이블에 저장)
* @access Private
*/
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 : "알 수 없는 오류",
});
}
}
);
router.post("/upload", uploadMiddleware, uploadFiles);
/**
* 파일 다운로드
* GET /api/files/download/:fileId
* @route GET /api/files
* @desc 파일 목록 조회
* @query targetObjid, docType, companyCode
* @access Private
*/
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 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,
companyCode,
userId: 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 : "알 수 없는 오류",
});
}
}
);
router.get("/", getFileList);
/**
* 파일 삭제
* DELETE /api/files/:fileId
* @route DELETE /api/files/:objid
* @desc 파일 삭제 (논리적 삭제)
* @access Private
*/
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 user = req.user;
const companyCode = user?.companyCode || "default";
// 먼저 회사별 폴더에서 찾기
let filePath = FileSystemManager.findFileInCompanyFolders(
companyCode,
serverFilename
);
// 찾지 못하면 기본 uploads 폴더에서 찾기 (하위 호환성)
if (!filePath) {
filePath = path.join(UPLOAD_PATH, serverFilename);
}
// 파일 존재 확인 및 삭제
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
logger.info("파일 삭제 완료", {
fileId,
serverFilename,
filePath,
companyCode,
userId: user?.userId,
});
} else {
logger.warn("삭제할 파일을 찾을 수 없음", {
fileId,
serverFilename,
companyCode,
userId: 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 : "알 수 없는 오류",
});
}
}
);
router.delete("/:objid", deleteFile);
/**
* 이미지 미리보기
* GET /api/files/preview/:fileId
* @route GET /api/files/download/:objid
* @desc 파일 다운로드
* @access Private
*/
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
*/
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 : "알 수 없는 오류",
});
}
}
);
router.get("/download/:objid", downloadFile);
export default router;

View File

@@ -185,6 +185,19 @@ export class AuthService {
},
});
// DB에서 조회한 원본 사용자 정보 상세 로그
console.log("🔍 AuthService - DB 원본 사용자 정보:", {
userId: userInfo.user_id,
company_code: userInfo.company_code,
company_code_type: typeof userInfo.company_code,
company_code_is_null: userInfo.company_code === null,
company_code_is_undefined: userInfo.company_code === undefined,
company_code_is_empty: userInfo.company_code === "",
dept_code: userInfo.dept_code,
allUserFields: Object.keys(userInfo),
companyInfo: companyInfo?.company_name,
});
// PersonBean 형태로 변환 (null 값을 undefined로 변환)
const personBean: PersonBean = {
userId: userInfo.user_id,
@@ -209,6 +222,12 @@ export class AuthService {
locale: userInfo.locale || "KR",
};
console.log("📦 AuthService - 최종 PersonBean:", {
userId: personBean.userId,
companyCode: personBean.companyCode,
deptCode: personBean.deptCode,
});
logger.info(`사용자 정보 조회 완료: ${userId}`);
return personBean;
} catch (error) {

View File

@@ -529,6 +529,121 @@ export class TableManagementService {
}
}
/**
* 파일 데이터 보강 (attach_file_info에서 파일 정보 가져오기)
*/
private async enrichFileData(
data: any[],
fileColumns: string[],
tableName: string
): Promise<any[]> {
try {
logger.info(
`파일 데이터 보강 시작: ${tableName}, ${fileColumns.join(", ")}`
);
// 각 행의 파일 정보를 보강
const enrichedData = await Promise.all(
data.map(async (row) => {
const enrichedRow = { ...row };
// 각 파일 컬럼에 대해 처리
for (const fileColumn of fileColumns) {
const filePath = row[fileColumn];
if (filePath && typeof filePath === "string") {
// 파일 경로에서 실제 파일 정보 조회
const fileInfo = await this.getFileInfoByPath(filePath);
if (fileInfo) {
// 파일 정보를 JSON 형태로 저장
enrichedRow[fileColumn] = JSON.stringify({
files: [fileInfo],
totalCount: 1,
totalSize: fileInfo.size,
});
}
}
}
return enrichedRow;
})
);
logger.info(`파일 데이터 보강 완료: ${enrichedData.length}개 행 처리`);
return enrichedData;
} catch (error) {
logger.error("파일 데이터 보강 실패:", error);
return data; // 실패 시 원본 데이터 반환
}
}
/**
* 파일 경로로 파일 정보 조회
*/
private async getFileInfoByPath(filePath: string): Promise<any | null> {
try {
const fileInfo = await prisma.attach_file_info.findFirst({
where: {
file_path: filePath,
status: "ACTIVE",
},
select: {
objid: true,
real_file_name: true,
file_size: true,
file_ext: true,
file_path: true,
doc_type: true,
doc_type_name: true,
regdate: true,
writer: true,
},
});
if (!fileInfo) {
return null;
}
return {
name: fileInfo.real_file_name,
path: fileInfo.file_path,
size: Number(fileInfo.file_size) || 0,
type: fileInfo.file_ext,
objid: fileInfo.objid.toString(),
docType: fileInfo.doc_type,
docTypeName: fileInfo.doc_type_name,
regdate: fileInfo.regdate?.toISOString(),
writer: fileInfo.writer,
};
} catch (error) {
logger.warn(`파일 정보 조회 실패: ${filePath}`, error);
return null;
}
}
/**
* 파일 타입 컬럼 조회
*/
private async getFileTypeColumns(tableName: string): Promise<string[]> {
try {
const fileColumns = await prisma.column_labels.findMany({
where: {
table_name: tableName,
web_type: "file",
},
select: {
column_name: true,
},
});
const columnNames = fileColumns.map((col: any) => col.column_name);
logger.info(`파일 타입 컬럼 감지: ${tableName}`, columnNames);
return columnNames;
} catch (error) {
logger.warn(`파일 타입 컬럼 조회 실패: ${tableName}`, error);
return [];
}
}
/**
* 테이블 데이터 조회 (페이징 + 검색)
*/
@@ -554,6 +669,9 @@ export class TableManagementService {
logger.info(`테이블 데이터 조회: ${tableName}`, options);
// 🎯 파일 타입 컬럼 감지
const fileColumns = await this.getFileTypeColumns(tableName);
// WHERE 조건 구성
let whereConditions: string[] = [];
let searchValues: any[] = [];
@@ -610,13 +728,18 @@ export class TableManagementService {
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
`;
const data = await prisma.$queryRawUnsafe<any[]>(
let data = await prisma.$queryRawUnsafe<any[]>(
dataQuery,
...searchValues,
size,
offset
);
// 🎯 파일 컬럼이 있으면 파일 정보 보강
if (fileColumns.length > 0) {
data = await this.enrichFileData(data, fileColumns, safeTableName);
}
const totalPages = Math.ceil(total / size);
logger.info(