Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node
This commit is contained in:
@@ -61,8 +61,41 @@ const storage = multer.diskStorage({
|
||||
filename: (req, file, cb) => {
|
||||
// 타임스탬프_원본파일명 형태로 저장 (회사코드는 디렉토리로 분리됨)
|
||||
const timestamp = Date.now();
|
||||
const sanitizedName = file.originalname.replace(/[^a-zA-Z0-9.-]/g, "_");
|
||||
|
||||
console.log("📁 파일명 처리:", {
|
||||
originalname: file.originalname,
|
||||
encoding: file.encoding,
|
||||
mimetype: file.mimetype
|
||||
});
|
||||
|
||||
// UTF-8 인코딩 문제 해결: Buffer를 통한 올바른 디코딩
|
||||
let decodedName;
|
||||
try {
|
||||
// 파일명이 깨진 경우 Buffer를 통해 올바르게 디코딩
|
||||
const buffer = Buffer.from(file.originalname, 'latin1');
|
||||
decodedName = buffer.toString('utf8');
|
||||
console.log("📁 파일명 디코딩:", { original: file.originalname, decoded: decodedName });
|
||||
} catch (error) {
|
||||
// 디코딩 실패 시 원본 사용
|
||||
decodedName = file.originalname;
|
||||
console.log("📁 파일명 디코딩 실패, 원본 사용:", file.originalname);
|
||||
}
|
||||
|
||||
// 한국어를 포함한 유니코드 문자 보존하면서 안전한 파일명 생성
|
||||
// 위험한 문자만 제거: / \ : * ? " < > |
|
||||
const sanitizedName = decodedName
|
||||
.replace(/[\/\\:*?"<>|]/g, "_") // 파일시스템에서 금지된 문자만 치환
|
||||
.replace(/\s+/g, "_") // 공백을 언더스코어로 치환
|
||||
.replace(/_{2,}/g, "_"); // 연속된 언더스코어를 하나로 축약
|
||||
|
||||
const savedFileName = `${timestamp}_${sanitizedName}`;
|
||||
|
||||
console.log("📁 파일명 변환:", {
|
||||
original: file.originalname,
|
||||
sanitized: sanitizedName,
|
||||
saved: savedFileName
|
||||
});
|
||||
|
||||
cb(null, savedFileName);
|
||||
},
|
||||
});
|
||||
@@ -87,18 +120,64 @@ const upload = multer({
|
||||
|
||||
// 기본 허용 파일 타입
|
||||
const defaultAllowedTypes = [
|
||||
// 이미지 파일
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/gif",
|
||||
"text/html", // HTML 파일 추가
|
||||
"text/plain", // 텍스트 파일 추가
|
||||
"image/webp",
|
||||
"image/svg+xml",
|
||||
// 텍스트 파일
|
||||
"text/html",
|
||||
"text/plain",
|
||||
"text/markdown",
|
||||
"text/csv",
|
||||
"application/json",
|
||||
"application/xml",
|
||||
// PDF 파일
|
||||
"application/pdf",
|
||||
"application/msword",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"application/vnd.ms-excel",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"application/zip", // ZIP 파일 추가
|
||||
"application/x-zip-compressed", // ZIP 파일 (다른 MIME 타입)
|
||||
// Microsoft Office 파일
|
||||
"application/msword", // .doc
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document", // .docx
|
||||
"application/vnd.ms-excel", // .xls
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", // .xlsx
|
||||
"application/vnd.ms-powerpoint", // .ppt
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation", // .pptx
|
||||
// 한컴오피스 파일
|
||||
"application/x-hwp", // .hwp (한글)
|
||||
"application/haansofthwp", // .hwp (다른 MIME 타입)
|
||||
"application/vnd.hancom.hwp", // .hwp (또 다른 MIME 타입)
|
||||
"application/vnd.hancom.hwpx", // .hwpx (한글 2014+)
|
||||
"application/x-hwpml", // .hwpml (한글 XML)
|
||||
"application/vnd.hancom.hcdt", // .hcdt (한셀)
|
||||
"application/vnd.hancom.hpt", // .hpt (한쇼)
|
||||
"application/octet-stream", // .hwp, .hwpx (일반적인 바이너리 파일)
|
||||
// 압축 파일
|
||||
"application/zip",
|
||||
"application/x-zip-compressed",
|
||||
"application/x-rar-compressed",
|
||||
"application/x-7z-compressed",
|
||||
// 미디어 파일
|
||||
"video/mp4",
|
||||
"video/webm",
|
||||
"video/ogg",
|
||||
"audio/mp3",
|
||||
"audio/mpeg",
|
||||
"audio/wav",
|
||||
"audio/ogg",
|
||||
// Apple/맥 파일
|
||||
"application/vnd.apple.pages", // .pages (Pages)
|
||||
"application/vnd.apple.numbers", // .numbers (Numbers)
|
||||
"application/vnd.apple.keynote", // .keynote (Keynote)
|
||||
"application/x-iwork-pages-sffpages", // .pages (다른 MIME)
|
||||
"application/x-iwork-numbers-sffnumbers", // .numbers (다른 MIME)
|
||||
"application/x-iwork-keynote-sffkey", // .keynote (다른 MIME)
|
||||
"application/vnd.apple.installer+xml", // .pkg (맥 설치 파일)
|
||||
"application/x-apple-diskimage", // .dmg (맥 디스크 이미지)
|
||||
// 기타 문서
|
||||
"application/rtf", // .rtf
|
||||
"application/vnd.oasis.opendocument.text", // .odt
|
||||
"application/vnd.oasis.opendocument.spreadsheet", // .ods
|
||||
"application/vnd.oasis.opendocument.presentation", // .odp
|
||||
];
|
||||
|
||||
if (defaultAllowedTypes.includes(file.mimetype)) {
|
||||
@@ -161,9 +240,20 @@ export const uploadFiles = async (
|
||||
const savedFiles = [];
|
||||
|
||||
for (const file of files) {
|
||||
// 파일명 디코딩 (파일 저장 시와 동일한 로직)
|
||||
let decodedOriginalName;
|
||||
try {
|
||||
const buffer = Buffer.from(file.originalname, 'latin1');
|
||||
decodedOriginalName = buffer.toString('utf8');
|
||||
console.log("💾 DB 저장용 파일명 디코딩:", { original: file.originalname, decoded: decodedOriginalName });
|
||||
} catch (error) {
|
||||
decodedOriginalName = file.originalname;
|
||||
console.log("💾 DB 저장용 파일명 디코딩 실패, 원본 사용:", file.originalname);
|
||||
}
|
||||
|
||||
// 파일 확장자 추출
|
||||
const fileExt = path
|
||||
.extname(file.originalname)
|
||||
.extname(decodedOriginalName)
|
||||
.toLowerCase()
|
||||
.replace(".", "");
|
||||
|
||||
@@ -196,7 +286,7 @@ export const uploadFiles = async (
|
||||
),
|
||||
target_objid: finalTargetObjid,
|
||||
saved_file_name: file.filename,
|
||||
real_file_name: file.originalname,
|
||||
real_file_name: decodedOriginalName,
|
||||
doc_type: docType,
|
||||
doc_type_name: docTypeName,
|
||||
file_size: file.size,
|
||||
|
||||
145
backend-node/src/controllers/screenFileController.ts
Normal file
145
backend-node/src/controllers/screenFileController.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { AuthenticatedRequest } from '../middleware/authMiddleware';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import logger from '../utils/logger';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
/**
|
||||
* 화면 컴포넌트별 파일 정보 조회 및 복원
|
||||
*/
|
||||
export const getScreenComponentFiles = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const { screenId } = req.params;
|
||||
|
||||
logger.info(`화면 컴포넌트 파일 조회 시작: screenId=${screenId}`);
|
||||
|
||||
// screen_files: 접두사로 해당 화면의 모든 파일 조회
|
||||
const targetObjidPattern = `screen_files:${screenId}:%`;
|
||||
|
||||
const files = await prisma.attach_file_info.findMany({
|
||||
where: {
|
||||
target_objid: {
|
||||
startsWith: `screen_files:${screenId}:`
|
||||
},
|
||||
status: 'ACTIVE'
|
||||
},
|
||||
orderBy: {
|
||||
regdate: 'desc'
|
||||
}
|
||||
});
|
||||
|
||||
// 컴포넌트별로 파일 그룹화
|
||||
const componentFiles: { [componentId: string]: any[] } = {};
|
||||
|
||||
files.forEach(file => {
|
||||
// target_objid 형식: screen_files:screenId:componentId:fieldName
|
||||
const targetParts = file.target_objid?.split(':') || [];
|
||||
if (targetParts.length >= 3) {
|
||||
const componentId = targetParts[2];
|
||||
|
||||
if (!componentFiles[componentId]) {
|
||||
componentFiles[componentId] = [];
|
||||
}
|
||||
|
||||
componentFiles[componentId].push({
|
||||
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
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
logger.info(`화면 컴포넌트 파일 조회 완료: ${Object.keys(componentFiles).length}개 컴포넌트, 총 ${files.length}개 파일`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
componentFiles: componentFiles,
|
||||
totalFiles: files.length,
|
||||
componentCount: Object.keys(componentFiles).length
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('화면 컴포넌트 파일 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '화면 컴포넌트 파일 조회 중 오류가 발생했습니다.',
|
||||
error: error instanceof Error ? error.message : '알 수 없는 오류'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 특정 컴포넌트의 파일 목록 조회
|
||||
*/
|
||||
export const getComponentFiles = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const { screenId, componentId } = req.params;
|
||||
|
||||
logger.info(`컴포넌트 파일 조회: screenId=${screenId}, componentId=${componentId}`);
|
||||
|
||||
// target_objid 패턴: screen_files:screenId:componentId:*
|
||||
const targetObjidPattern = `screen_files:${screenId}:${componentId}:`;
|
||||
|
||||
const files = await prisma.attach_file_info.findMany({
|
||||
where: {
|
||||
target_objid: {
|
||||
startsWith: targetObjidPattern
|
||||
},
|
||||
status: 'ACTIVE'
|
||||
},
|
||||
orderBy: {
|
||||
regdate: 'desc'
|
||||
}
|
||||
});
|
||||
|
||||
const fileList = files.map(file => ({
|
||||
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
|
||||
}));
|
||||
|
||||
logger.info(`컴포넌트 파일 조회 완료: ${fileList.length}개 파일`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
files: fileList,
|
||||
componentId: componentId,
|
||||
screenId: screenId
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('컴포넌트 파일 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '컴포넌트 파일 조회 중 오류가 발생했습니다.',
|
||||
error: error instanceof Error ? error.message : '알 수 없는 오류'
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user