파일 업로드 기능 구현 및 상세설정 연동

- 템플릿 파일첨부 컴포넌트와 FileComponentConfigPanel 실시간 동기화
- FileUpload 위젯에 전역 파일 상태 관리 기능 추가
- 파일 업로드/삭제 시 전역 상태 및 localStorage 동기화
- RealtimePreview에서 전역 상태 우선 읽기 및 파일 개수 표시
- 한컴오피스, Apple iWork 파일 형식 지원 추가
- 파일 뷰어 모달 및 미리보기 기능 구현
- 업로드된 파일 디렉토리 .gitignore 추가
This commit is contained in:
leeheejin
2025-09-26 13:11:34 +09:00
parent c28e27f3e8
commit ee7c8e989e
20 changed files with 2661 additions and 503 deletions

View File

@@ -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,

View File

@@ -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;

View File

@@ -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() {}