From df779ac04cfbb9508bb992047dceb1dbf9fe6222 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Wed, 5 Nov 2025 15:50:29 +0900 Subject: [PATCH] =?UTF-8?q?=EB=8C=80=ED=91=9C=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=A0=80=EC=9E=A5=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/controllers/fileController.ts | 64 +++++++++++++++++++ backend-node/src/routes/fileRoutes.ts | 8 +++ frontend/lib/api/file.ts | 16 +++++ .../file-upload/FileUploadComponent.tsx | 57 ++++++----------- 4 files changed, 108 insertions(+), 37 deletions(-) diff --git a/backend-node/src/controllers/fileController.ts b/backend-node/src/controllers/fileController.ts index 78193960..b1e31e3b 100644 --- a/backend-node/src/controllers/fileController.ts +++ b/backend-node/src/controllers/fileController.ts @@ -616,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) => @@ -1088,5 +1089,68 @@ export const getFileByToken = async (req: Request, res: Response) => { } }; +/** + * 대표 파일 설정 + */ +export const setRepresentativeFile = async ( + req: AuthenticatedRequest, + res: Response +): Promise => { + try { + const { objid } = req.params; + const companyCode = req.user?.companyCode; + + // 파일 존재 여부 및 권한 확인 + const fileRecord = await queryOne( + `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( + `UPDATE attach_file_info + SET is_representative = false + WHERE target_objid = $1 AND objid != $2`, + [fileRecord.target_objid, parseInt(objid)] + ); + + // 선택한 파일을 대표 파일로 설정 + await query( + `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개 파일 diff --git a/backend-node/src/routes/fileRoutes.ts b/backend-node/src/routes/fileRoutes.ts index e62d479a..64f02d14 100644 --- a/backend-node/src/routes/fileRoutes.ts +++ b/backend-node/src/routes/fileRoutes.ts @@ -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; diff --git a/frontend/lib/api/file.ts b/frontend/lib/api/file.ts index 8cba4e60..318f26fb 100644 --- a/frontend/lib/api/file.ts +++ b/frontend/lib/api/file.ts @@ -249,3 +249,19 @@ export const getDirectFileUrl = (filePath: string): string => { const baseUrl = process.env.NEXT_PUBLIC_API_URL?.replace("/api", "") || ""; return `${baseUrl}${filePath}`; }; + +/** + * 대표 파일 설정 + */ +export const setRepresentativeFile = async (objid: string): Promise<{ + success: boolean; + message: string; +}> => { + try { + const response = await apiClient.put(`/files/representative/${objid}`); + return response.data; + } catch (error) { + console.error("대표 파일 설정 오류:", error); + throw new Error("대표 파일 설정에 실패했습니다."); + } +}; diff --git a/frontend/lib/registry/components/file-upload/FileUploadComponent.tsx b/frontend/lib/registry/components/file-upload/FileUploadComponent.tsx index 2ae20180..f5eefe3c 100644 --- a/frontend/lib/registry/components/file-upload/FileUploadComponent.tsx +++ b/frontend/lib/registry/components/file-upload/FileUploadComponent.tsx @@ -802,50 +802,33 @@ const FileUploadComponent: React.FC = ({ // 대표 이미지 설정 핸들러 const handleSetRepresentative = useCallback( - (file: FileInfo) => { - const updatedFiles = uploadedFiles.map((f) => ({ - ...f, - isRepresentative: f.objid === file.objid, - })); - - setUploadedFiles(updatedFiles); - - // 대표 이미지 로드 - loadRepresentativeImage(file); - - // localStorage 백업 + async (file: FileInfo) => { try { - const backupKey = `fileUpload_${component.id}`; - localStorage.setItem(backupKey, JSON.stringify(updatedFiles)); - console.log("📌 대표 파일 설정 완료:", { + // API 호출하여 DB에 대표 파일 설정 + const { setRepresentativeFile } = await import("@/lib/api/file"); + await setRepresentativeFile(file.objid); + + // 상태 업데이트 + const updatedFiles = uploadedFiles.map((f) => ({ + ...f, + isRepresentative: f.objid === file.objid, + })); + + setUploadedFiles(updatedFiles); + + // 대표 이미지 로드 + loadRepresentativeImage(file); + + console.log("✅ 대표 파일 설정 완료:", { componentId: component.id, representativeFile: file.realFileName, + objid: file.objid, }); } catch (e) { - console.warn("localStorage 저장 실패:", e); + console.error("❌ 대표 파일 설정 실패:", e); } - - // 전역 상태 동기화 - if (typeof window !== "undefined") { - (window as any).globalFileState = { - ...(window as any).globalFileState, - [component.id]: updatedFiles, - }; - - // 실시간 동기화 이벤트 발송 - const syncEvent = new CustomEvent("fileStateChanged", { - detail: { - componentId: component.id, - files: updatedFiles, - action: "setRepresentative", - }, - }); - window.dispatchEvent(syncEvent); - } - - toast.success(`${file.realFileName}을(를) 대표 파일로 설정했습니다.`); }, - [uploadedFiles, component.id, loadRepresentativeImage], + [uploadedFiles, component.id, loadRepresentativeImage] ); // uploadedFiles 변경 시 대표 이미지 로드