From fcf887ae760a7a0df798f142dae571bc45f74add Mon Sep 17 00:00:00 2001 From: kjs Date: Wed, 1 Oct 2025 14:37:33 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20pool=20export=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=EB=A1=9C=20buttonActionStandardController=20=EC=BB=B4=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=97=90=EB=9F=AC=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 문제: - buttonActionStandardController에서 pool을 import하려 했으나 - db.ts에서 pool이 export되지 않아 컴파일 에러 발생 해결: - db.ts에 'export { pool }' 추가 - pool 직접 접근이 필요한 경우를 위해 명시적 export 영향받는 파일: - backend-node/src/database/db.ts - backend-node/src/controllers/buttonActionStandardController.ts (사용) --- .../src/controllers/fileController.ts | 157 ++++++++++++------ backend-node/src/database/db.ts | 3 + 2 files changed, 107 insertions(+), 53 deletions(-) diff --git a/backend-node/src/controllers/fileController.ts b/backend-node/src/controllers/fileController.ts index 2856098b..d138bce3 100644 --- a/backend-node/src/controllers/fileController.ts +++ b/backend-node/src/controllers/fileController.ts @@ -62,41 +62,44 @@ const storage = multer.diskStorage({ filename: (req, file, cb) => { // 타임스탬프_원본파일명 형태로 저장 (회사코드는 디렉토리로 분리됨) const timestamp = Date.now(); - + console.log("📁 파일명 처리:", { originalname: file.originalname, encoding: file.encoding, - mimetype: file.mimetype + 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 }); + 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, "_"); // 연속된 언더스코어를 하나로 축약 - + .replace(/[\/\\:*?"<>|]/g, "_") // 파일시스템에서 금지된 문자만 치환 + .replace(/\s+/g, "_") // 공백을 언더스코어로 치환 + .replace(/_{2,}/g, "_"); // 연속된 언더스코어를 하나로 축약 + const savedFileName = `${timestamp}_${sanitizedName}`; - + console.log("📁 파일명 변환:", { original: file.originalname, sanitized: sanitizedName, - saved: savedFileName + saved: savedFileName, }); - + cb(null, savedFileName); }, }); @@ -167,7 +170,7 @@ const upload = multer({ "audio/ogg", // Apple/맥 파일 "application/vnd.apple.pages", // .pages (Pages) - "application/vnd.apple.numbers", // .numbers (Numbers) + "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) @@ -244,14 +247,20 @@ export const uploadFiles = async ( // 파일명 디코딩 (파일 저장 시와 동일한 로직) let decodedOriginalName; try { - const buffer = Buffer.from(file.originalname, 'latin1'); - decodedOriginalName = buffer.toString('utf8'); - console.log("💾 DB 저장용 파일명 디코딩:", { original: file.originalname, decoded: decodedOriginalName }); + 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); + console.log( + "💾 DB 저장용 파일명 디코딩 실패, 원본 사용:", + file.originalname + ); } - + // 파일 확장자 추출 const fileExt = path .extname(decodedOriginalName) @@ -267,7 +276,7 @@ export const uploadFiles = async ( // 회사코드가 *인 경우 company_*로 변환 const actualCompanyCode = companyCode === "*" ? "company_*" : companyCode; - + // 임시 파일을 최종 위치로 이동 const tempFilePath = file.path; // Multer가 저장한 임시 파일 경로 const finalUploadDir = getCompanyUploadDir(companyCode, dateFolder); @@ -293,8 +302,20 @@ export const uploadFiles = async ( ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING *`, [ - objidValue, finalTargetObjid, file.filename, decodedOriginalName, docType, docTypeName, - file.size, fileExt, fullFilePath, companyCode, writer, new Date(), "ACTIVE", parentTargetObjid + objidValue, + finalTargetObjid, + file.filename, + decodedOriginalName, + docType, + docTypeName, + file.size, + fileExt, + fullFilePath, + companyCode, + writer, + new Date(), + "ACTIVE", + parentTargetObjid, ] ); @@ -486,15 +507,16 @@ export const getComponentFiles = async ( res: Response ): Promise => { try { - const { screenId, componentId, tableName, recordId, columnName } = req.query; - + const { screenId, componentId, tableName, recordId, columnName } = + req.query; + console.log("📂 [getComponentFiles] API 호출:", { screenId, componentId, tableName, recordId, columnName, - user: req.user?.userId + user: req.user?.userId, }); if (!screenId || !componentId) { @@ -507,9 +529,11 @@ export const getComponentFiles = async ( } // 1. 템플릿 파일 조회 (화면 설계 시 업로드한 파일들) - const templateTargetObjid = `screen_files:${screenId}:${componentId}:${columnName || 'field_1'}`; - console.log("🔍 [getComponentFiles] 템플릿 파일 조회:", { templateTargetObjid }); - + const templateTargetObjid = `screen_files:${screenId}:${componentId}:${columnName || "field_1"}`; + console.log("🔍 [getComponentFiles] 템플릿 파일 조회:", { + templateTargetObjid, + }); + // 모든 파일 조회해서 실제 저장된 target_objid 패턴 확인 const allFiles = await query( `SELECT target_objid, real_file_name, regdate @@ -519,7 +543,13 @@ export const getComponentFiles = async ( LIMIT 10`, ["ACTIVE"] ); - console.log("🗂️ [getComponentFiles] 최근 저장된 파일들의 target_objid:", allFiles.map(f => ({ target_objid: f.target_objid, name: f.real_file_name }))); + console.log( + "🗂️ [getComponentFiles] 최근 저장된 파일들의 target_objid:", + allFiles.map((f) => ({ + target_objid: f.target_objid, + name: f.real_file_name, + })) + ); const templateFiles = await query( `SELECT * FROM attach_file_info @@ -527,8 +557,11 @@ export const getComponentFiles = async ( ORDER BY regdate DESC`, [templateTargetObjid, "ACTIVE"] ); - - console.log("📁 [getComponentFiles] 템플릿 파일 결과:", templateFiles.length); + + console.log( + "📁 [getComponentFiles] 템플릿 파일 결과:", + templateFiles.length + ); // 2. 데이터 파일 조회 (실제 레코드와 연결된 파일들) let dataFiles: any[] = []; @@ -560,13 +593,18 @@ export const getComponentFiles = async ( isTemplate, // 템플릿 파일 여부 표시 }); - const formattedTemplateFiles = templateFiles.map(file => formatFileInfo(file, true)); - const formattedDataFiles = dataFiles.map(file => formatFileInfo(file, false)); + const formattedTemplateFiles = templateFiles.map((file) => + formatFileInfo(file, true) + ); + const formattedDataFiles = dataFiles.map((file) => + formatFileInfo(file, false) + ); // 3. 전체 파일 목록 (데이터 파일 우선, 없으면 템플릿 파일 표시) - const totalFiles = formattedDataFiles.length > 0 - ? formattedDataFiles - : formattedTemplateFiles; + const totalFiles = + formattedDataFiles.length > 0 + ? formattedDataFiles + : formattedTemplateFiles; res.json({ success: true, @@ -578,9 +616,10 @@ export const getComponentFiles = async ( dataCount: formattedDataFiles.length, totalCount: totalFiles.length, templateTargetObjid, - dataTargetObjid: tableName && recordId && columnName - ? `${tableName}:${recordId}:${columnName}` - : null, + dataTargetObjid: + tableName && recordId && columnName + ? `${tableName}:${recordId}:${columnName}` + : null, }, }); } catch (error) { @@ -620,12 +659,12 @@ export const previewFile = async ( // 파일 경로에서 회사코드와 날짜 폴더 추출 const filePathParts = fileRecord.file_path!.split("/"); let companyCode = filePathParts[2] || "DEFAULT"; - + // company_* 처리 (실제 회사 코드로 변환) if (companyCode === "company_*") { companyCode = "company_*"; // 실제 디렉토리명 유지 } - + const fileName = fileRecord.saved_file_name!; // 파일 경로에 날짜 구조가 있는지 확인 (YYYY/MM/DD) @@ -648,7 +687,7 @@ export const previewFile = async ( fileName: fileName, companyUploadDir: companyUploadDir, finalFilePath: filePath, - fileExists: fs.existsSync(filePath) + fileExists: fs.existsSync(filePath), }); if (!fs.existsSync(filePath)) { @@ -739,12 +778,12 @@ export const downloadFile = async ( // 파일 경로에서 회사코드와 날짜 폴더 추출 (예: /uploads/company_*/2025/09/05/timestamp_filename.ext) const filePathParts = fileRecord.file_path!.split("/"); let companyCode = filePathParts[2] || "DEFAULT"; // /uploads/company_*/2025/09/05/filename.ext에서 company_* 추출 - + // company_* 처리 (실제 회사 코드로 변환) if (companyCode === "company_*") { companyCode = "company_*"; // 실제 디렉토리명 유지 } - + const fileName = fileRecord.saved_file_name!; // 파일 경로에 날짜 구조가 있는지 확인 (YYYY/MM/DD) @@ -768,7 +807,7 @@ export const downloadFile = async ( fileName: fileName, companyUploadDir: companyUploadDir, finalFilePath: filePath, - fileExists: fs.existsSync(filePath) + fileExists: fs.existsSync(filePath), }); if (!fs.existsSync(filePath)) { @@ -803,7 +842,10 @@ export const downloadFile = async ( /** * Google Docs Viewer용 임시 공개 토큰 생성 */ -export const generateTempToken = async (req: AuthenticatedRequest, res: Response) => { +export const generateTempToken = async ( + req: AuthenticatedRequest, + res: Response +) => { try { const { objid } = req.params; @@ -923,7 +965,10 @@ export const getFileByToken = async (req: Request, res: Response) => { if (filePathParts.length >= 6) { dateFolder = `${filePathParts[3]}/${filePathParts[4]}/${filePathParts[5]}`; } - const companyUploadDir = getCompanyUploadDir(companyCode, dateFolder || undefined); + const companyUploadDir = getCompanyUploadDir( + companyCode, + dateFolder || undefined + ); const filePath = path.join(companyUploadDir, fileName); // 파일 존재 확인 @@ -938,15 +983,18 @@ export const getFileByToken = async (req: Request, res: Response) => { // MIME 타입 설정 const ext = path.extname(fileName).toLowerCase(); let contentType = "application/octet-stream"; - + const mimeTypes: { [key: string]: string } = { ".pdf": "application/pdf", ".doc": "application/msword", - ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + ".docx": + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", ".xls": "application/vnd.ms-excel", - ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + ".xlsx": + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ".ppt": "application/vnd.ms-powerpoint", - ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation", + ".pptx": + "application/vnd.openxmlformats-officedocument.presentationml.presentation", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", @@ -960,7 +1008,10 @@ export const getFileByToken = async (req: Request, res: Response) => { // 파일 헤더 설정 res.setHeader("Content-Type", contentType); - res.setHeader("Content-Disposition", `inline; filename="${encodeURIComponent(fileRecord.real_file_name!)}"`); + res.setHeader( + "Content-Disposition", + `inline; filename="${encodeURIComponent(fileRecord.real_file_name!)}"` + ); res.setHeader("Cache-Control", "public, max-age=300"); // 5분 캐시 // 파일 스트림 전송 diff --git a/backend-node/src/database/db.ts b/backend-node/src/database/db.ts index cd5f5142..ae775525 100644 --- a/backend-node/src/database/db.ts +++ b/backend-node/src/database/db.ts @@ -259,6 +259,9 @@ export function getPoolStatus() { }; } +// Pool 직접 접근 (필요한 경우) +export { pool }; + // 기본 익스포트 (편의성) export default { query,