파일 업로드 기능 구현 및 상세설정 연동
- 템플릿 파일첨부 컴포넌트와 FileComponentConfigPanel 실시간 동기화 - FileUpload 위젯에 전역 파일 상태 관리 기능 추가 - 파일 업로드/삭제 시 전역 상태 및 localStorage 동기화 - RealtimePreview에서 전역 상태 우선 읽기 및 파일 개수 표시 - 한컴오피스, Apple iWork 파일 형식 지원 추가 - 파일 뷰어 모달 및 미리보기 기능 구현 - 업로드된 파일 디렉토리 .gitignore 추가
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,
|
||||
|
||||
@@ -8,11 +8,9 @@ import { ExternalDbConnectionService } from "./externalDbConnectionService";
|
||||
import { TableManagementService } from "./tableManagementService";
|
||||
import { ExternalDbConnection } from "../types/externalDbTypes";
|
||||
import { ColumnTypeInfo, TableInfo } from "../types/tableManagement";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import prisma from "../config/database";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export interface ValidationResult {
|
||||
isValid: boolean;
|
||||
error?: string;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import prisma from "../config/database";
|
||||
import { logger } from "../utils/logger";
|
||||
import { cache, CacheKeys } from "../utils/cache";
|
||||
import {
|
||||
@@ -14,8 +14,6 @@ import { WebType } from "../types/unified-web-types";
|
||||
import { entityJoinService } from "./entityJoinService";
|
||||
import { referenceCacheService } from "./referenceCacheService";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export class TableManagementService {
|
||||
constructor() {}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user