fix: pool export 추가로 buttonActionStandardController 컴파일 에러 해결

문제:
- 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 (사용)
This commit is contained in:
kjs 2025-10-01 14:37:33 +09:00
parent f2f0c33bad
commit fcf887ae76
2 changed files with 107 additions and 53 deletions

View File

@ -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<void> => {
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<any>(
`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<any>(
`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분 캐시
// 파일 스트림 전송

View File

@ -259,6 +259,9 @@ export function getPoolStatus() {
};
}
// Pool 직접 접근 (필요한 경우)
export { pool };
// 기본 익스포트 (편의성)
export default {
query,