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:
parent
f2f0c33bad
commit
fcf887ae76
|
|
@ -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분 캐시
|
||||
|
||||
// 파일 스트림 전송
|
||||
|
|
|
|||
|
|
@ -259,6 +259,9 @@ export function getPoolStatus() {
|
|||
};
|
||||
}
|
||||
|
||||
// Pool 직접 접근 (필요한 경우)
|
||||
export { pool };
|
||||
|
||||
// 기본 익스포트 (편의성)
|
||||
export default {
|
||||
query,
|
||||
|
|
|
|||
Loading…
Reference in New Issue