Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management
This commit is contained in:
@@ -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개 파일
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user