Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management

This commit is contained in:
kjs
2025-11-06 11:58:09 +09:00
16 changed files with 749 additions and 465 deletions
+159 -33
View File
@@ -232,7 +232,11 @@ export const uploadFiles = async (
// 자동 연결 로직 - target_objid 자동 생성
let finalTargetObjid = targetObjid;
if (autoLink === "true" && linkedTable && recordId) {
// 🔑 템플릿 파일(screen_files:)이나 temp_ 파일은 autoLink 무시
const isTemplateFile = targetObjid && (targetObjid.startsWith('screen_files:') || targetObjid.startsWith('temp_'));
if (!isTemplateFile && autoLink === "true" && linkedTable && recordId) {
// 가상 파일 컬럼의 경우 컬럼명도 포함한 target_objid 생성
if (isVirtualFileColumn === "true" && columnName) {
finalTargetObjid = `${linkedTable}:${recordId}:${columnName}`;
@@ -363,6 +367,38 @@ export const deleteFile = async (
const { objid } = req.params;
const { writer = "system" } = req.body;
// 🔒 멀티테넌시: 현재 사용자의 회사 코드
const companyCode = req.user?.companyCode;
// 파일 정보 조회
const fileRecord = await queryOne<any>(
`SELECT * FROM attach_file_info WHERE objid = $1`,
[parseInt(objid)]
);
if (!fileRecord) {
res.status(404).json({
success: false,
message: "파일을 찾을 수 없습니다.",
});
return;
}
// 🔒 멀티테넌시: 회사 코드 일치 여부 확인 (최고 관리자 제외)
if (companyCode !== "*" && fileRecord.company_code !== companyCode) {
console.warn("⚠️ 다른 회사 파일 삭제 시도:", {
userId: req.user?.userId,
userCompanyCode: companyCode,
fileCompanyCode: fileRecord.company_code,
objid,
});
res.status(403).json({
success: false,
message: "접근 권한이 없습니다.",
});
return;
}
// 파일 상태를 DELETED로 변경 (논리적 삭제)
await query<any>(
"UPDATE attach_file_info SET status = $1 WHERE objid = $2",
@@ -510,6 +546,9 @@ export const getComponentFiles = async (
const { screenId, componentId, tableName, recordId, columnName } =
req.query;
// 🔒 멀티테넌시: 현재 사용자의 회사 코드 가져오기
const companyCode = req.user?.companyCode;
console.log("📂 [getComponentFiles] API 호출:", {
screenId,
componentId,
@@ -517,6 +556,7 @@ export const getComponentFiles = async (
recordId,
columnName,
user: req.user?.userId,
companyCode, // 🔒 멀티테넌시 로그
});
if (!screenId || !componentId) {
@@ -534,32 +574,16 @@ export const getComponentFiles = async (
templateTargetObjid,
});
// 모든 파일 조회해서 실제 저장된 target_objid 패턴 확인
const allFiles = await query<any>(
`SELECT target_objid, real_file_name, regdate
FROM attach_file_info
WHERE status = $1
ORDER BY regdate DESC
LIMIT 10`,
["ACTIVE"]
);
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
WHERE target_objid = $1 AND status = $2
WHERE target_objid = $1 AND status = $2 AND company_code = $3
ORDER BY regdate DESC`,
[templateTargetObjid, "ACTIVE"]
[templateTargetObjid, "ACTIVE", companyCode]
);
console.log(
"📁 [getComponentFiles] 템플릿 파일 결과:",
"📁 [getComponentFiles] 템플릿 파일 결과 (회사별 필터링):",
templateFiles.length
);
@@ -567,11 +591,12 @@ export const getComponentFiles = async (
let dataFiles: any[] = [];
if (tableName && recordId && columnName) {
const dataTargetObjid = `${tableName}:${recordId}:${columnName}`;
// 🔒 멀티테넌시: 회사별 필터링 추가
dataFiles = await query<any>(
`SELECT * FROM attach_file_info
WHERE target_objid = $1 AND status = $2
WHERE target_objid = $1 AND status = $2 AND company_code = $3
ORDER BY regdate DESC`,
[dataTargetObjid, "ACTIVE"]
[dataTargetObjid, "ACTIVE", companyCode]
);
}
@@ -591,6 +616,7 @@ export const getComponentFiles = async (
regdate: file.regdate?.toISOString(),
status: file.status,
isTemplate, // 템플릿 파일 여부 표시
isRepresentative: file.is_representative || false, // 대표 파일 여부
});
const formattedTemplateFiles = templateFiles.map((file) =>
@@ -643,6 +669,9 @@ export const previewFile = async (
const { objid } = req.params;
const { serverFilename } = req.query;
// 🔒 멀티테넌시: 현재 사용자의 회사 코드
const companyCode = req.user?.companyCode;
const fileRecord = await queryOne<any>(
"SELECT * FROM attach_file_info WHERE objid = $1 LIMIT 1",
[parseInt(objid)]
@@ -656,13 +685,28 @@ export const previewFile = async (
return;
}
// 🔒 멀티테넌시: 회사 코드 일치 여부 확인 (최고 관리자 제외)
if (companyCode !== "*" && fileRecord.company_code !== companyCode) {
console.warn("⚠️ 다른 회사 파일 접근 시도:", {
userId: req.user?.userId,
userCompanyCode: companyCode,
fileCompanyCode: fileRecord.company_code,
objid,
});
res.status(403).json({
success: false,
message: "접근 권한이 없습니다.",
});
return;
}
// 파일 경로에서 회사코드와 날짜 폴더 추출
const filePathParts = fileRecord.file_path!.split("/");
let companyCode = filePathParts[2] || "DEFAULT";
let fileCompanyCode = filePathParts[2] || "DEFAULT";
// company_* 처리 (실제 회사 코드로 변환)
if (companyCode === "company_*") {
companyCode = "company_*"; // 실제 디렉토리명 유지
if (fileCompanyCode === "company_*") {
fileCompanyCode = "company_*"; // 실제 디렉토리명 유지
}
const fileName = fileRecord.saved_file_name!;
@@ -674,7 +718,7 @@ export const previewFile = async (
}
const companyUploadDir = getCompanyUploadDir(
companyCode,
fileCompanyCode,
dateFolder || undefined
);
const filePath = path.join(companyUploadDir, fileName);
@@ -724,8 +768,9 @@ export const previewFile = async (
mimeType = "application/octet-stream";
}
// CORS 헤더 설정 (더 포괄적으로)
res.setHeader("Access-Control-Allow-Origin", "*");
// CORS 헤더 설정 (credentials 모드에서는 구체적인 origin 필요)
const origin = req.headers.origin || "http://localhost:9771";
res.setHeader("Access-Control-Allow-Origin", origin);
res.setHeader(
"Access-Control-Allow-Methods",
"GET, POST, PUT, DELETE, OPTIONS"
@@ -762,6 +807,9 @@ export const downloadFile = async (
try {
const { objid } = req.params;
// 🔒 멀티테넌시: 현재 사용자의 회사 코드
const companyCode = req.user?.companyCode;
const fileRecord = await queryOne<any>(
`SELECT * FROM attach_file_info WHERE objid = $1`,
[parseInt(objid)]
@@ -775,13 +823,28 @@ export const downloadFile = async (
return;
}
// 🔒 멀티테넌시: 회사 코드 일치 여부 확인 (최고 관리자 제외)
if (companyCode !== "*" && fileRecord.company_code !== companyCode) {
console.warn("⚠️ 다른 회사 파일 다운로드 시도:", {
userId: req.user?.userId,
userCompanyCode: companyCode,
fileCompanyCode: fileRecord.company_code,
objid,
});
res.status(403).json({
success: false,
message: "접근 권한이 없습니다.",
});
return;
}
// 파일 경로에서 회사코드와 날짜 폴더 추출 (예: /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_* 추출
let fileCompanyCode = filePathParts[2] || "DEFAULT"; // /uploads/company_*/2025/09/05/filename.ext에서 company_* 추출
// company_* 처리 (실제 회사 코드로 변환)
if (companyCode === "company_*") {
companyCode = "company_*"; // 실제 디렉토리명 유지
if (fileCompanyCode === "company_*") {
fileCompanyCode = "company_*"; // 실제 디렉토리명 유지
}
const fileName = fileRecord.saved_file_name!;
@@ -794,7 +857,7 @@ export const downloadFile = async (
}
const companyUploadDir = getCompanyUploadDir(
companyCode,
fileCompanyCode,
dateFolder || undefined
);
const filePath = path.join(companyUploadDir, fileName);
@@ -1026,5 +1089,68 @@ export const getFileByToken = async (req: Request, res: Response) => {
}
};
/**
* 대표 파일 설정
*/
export const setRepresentativeFile = async (
req: AuthenticatedRequest,
res: Response
): Promise<void> => {
try {
const { objid } = req.params;
const companyCode = req.user?.companyCode;
// 파일 존재 여부 및 권한 확인
const fileRecord = await queryOne<any>(
`SELECT * FROM attach_file_info WHERE objid = $1 AND status = $2`,
[parseInt(objid), "ACTIVE"]
);
if (!fileRecord) {
res.status(404).json({
success: false,
message: "파일을 찾을 수 없습니다.",
});
return;
}
// 멀티테넌시: 회사 코드 확인
if (companyCode !== "*" && fileRecord.company_code !== companyCode) {
res.status(403).json({
success: false,
message: "접근 권한이 없습니다.",
});
return;
}
// 같은 target_objid의 다른 파일들의 is_representative를 false로 설정
await query<any>(
`UPDATE attach_file_info
SET is_representative = false
WHERE target_objid = $1 AND objid != $2`,
[fileRecord.target_objid, parseInt(objid)]
);
// 선택한 파일을 대표 파일로 설정
await query<any>(
`UPDATE attach_file_info
SET is_representative = true
WHERE objid = $1`,
[parseInt(objid)]
);
res.json({
success: true,
message: "대표 파일이 설정되었습니다.",
});
} catch (error) {
console.error("대표 파일 설정 오류:", error);
res.status(500).json({
success: false,
message: "대표 파일 설정 중 오류가 발생했습니다.",
});
}
};
// Multer 미들웨어 export
export const uploadMiddleware = upload.array("files", 10); // 최대 10개 파일
+8
View File
@@ -10,6 +10,7 @@ import {
uploadMiddleware,
generateTempToken,
getFileByToken,
setRepresentativeFile,
} from "../controllers/fileController";
import { authenticateToken } from "../middleware/authMiddleware";
@@ -84,4 +85,11 @@ router.get("/download/:objid", downloadFile);
*/
router.post("/temp-token/:objid", generateTempToken);
/**
* @route PUT /api/files/representative/:objid
* @desc 대표 파일 설정
* @access Private
*/
router.put("/representative/:objid", setRepresentativeFile);
export default router;