import { apiClient, API_BASE_URL } from "./client"; export interface FileInfo { id: string; name: string; size: number; type: string; extension: string; uploaded_at: string; last_modified: string; server_path?: string; server_filename?: string; } export interface FileUploadResponse { success: boolean; message: string; files: FileInfo[]; data?: FileInfo[]; } export interface FileDownloadParams { fileId: string; server_filename: string; original_name: string; } /** * 파일 업로드 */ export const uploadFiles = async (params: { files: FileList | File[]; tableName?: string; fieldName?: string; recordId?: string | number; docType?: string; docTypeName?: string; targetObjid?: string; parentTargetObjid?: string; linkedTable?: string; linkedField?: string; autoLink?: boolean; columnName?: string; isVirtualFileColumn?: boolean; company_code?: string; // 🔒 멀티테넌시: 회사 코드 isRecordMode?: boolean; // 🆕 레코드 모드 플래그 }): Promise => { const formData = new FormData(); // 파일 추가 const fileArray = Array.isArray(params.files) ? params.files : Array.from(params.files); fileArray.forEach((file) => { formData.append("files", file); }); // 추가 파라미터들 추가 if (params.tableName) formData.append("table_name", params.tableName); if (params.fieldName) formData.append("field_name", params.fieldName); if (params.recordId) formData.append("record_id", String(params.recordId)); if (params.docType) formData.append("doc_type", params.docType); if (params.docTypeName) formData.append("doc_type_name", params.docTypeName); if (params.targetObjid) formData.append("target_objid", params.targetObjid); if (params.parentTargetObjid) formData.append("parent_target_objid", params.parentTargetObjid); if (params.linkedTable) formData.append("linked_table", params.linkedTable); if (params.linkedField) formData.append("linked_field", params.linkedField); if (params.autoLink !== undefined) formData.append("auto_link", params.autoLink.toString()); if (params.columnName) formData.append("column_name", params.columnName); if (params.isVirtualFileColumn !== undefined) formData.append("is_virtual_file_column", params.isVirtualFileColumn.toString()); if (params.company_code) formData.append("company_code", params.company_code); // 🔒 멀티테넌시 // 🆕 레코드 모드 플래그 추가 (백엔드에서 attachments 컬럼 자동 업데이트용) if (params.isRecordMode !== undefined) formData.append("is_record_mode", params.isRecordMode.toString()); const response = await apiClient.post("/files/upload", formData, { headers: { "Content-Type": undefined, // axios가 자동으로 multipart/form-data를 설정하도록 }, }); return response.data; }; /** * 파일 다운로드 */ export const downloadFile = async (params: FileDownloadParams): Promise => { try { console.log("📥 downloadFile 호출:", params); const response = await apiClient.get(`/files/download/${params.fileId}`, { params: { server_filename: params.server_filename, original_name: params.original_name, }, responseType: "blob", // 파일 다운로드를 위해 blob 타입으로 설정 }); console.log("📥 다운로드 응답:", response); // Blob URL 생성 const blob = new Blob([response.data]); const url = window.URL.createObjectURL(blob); // 다운로드 링크 생성 및 클릭 const link = document.createElement("a"); link.href = url; link.download = params.original_name; document.body.appendChild(link); link.click(); // 정리 document.body.removeChild(link); window.URL.revokeObjectURL(url); } catch (error) { console.error("파일 다운로드 오류:", error); throw new Error("파일 다운로드에 실패했습니다."); } }; /** * 파일 삭제 */ export const deleteFile = async (fileId: string, serverFilename: string): Promise => { const response = await apiClient.delete(`/files/${fileId}`, { data: { server_filename: serverFilename }, }); if (!response.data.success) { throw new Error(response.data.message || "파일 삭제에 실패했습니다."); } }; /** * 파일 정보 조회 */ export const getFileInfo = async (fileId: string, serverFilename: string) => { const response = await apiClient.get(`/files/info/${fileId}`, { params: { server_filename: serverFilename }, }); return response.data; }; /** * 컴포넌트의 템플릿 파일과 데이터 파일을 모두 조회 */ export const getComponentFiles = async (params: { screen_id: number; component_id: string; table_name?: string; record_id?: string; column_name?: string; }): Promise<{ success: boolean; templateFiles: FileInfo[]; dataFiles: FileInfo[]; totalFiles: FileInfo[]; }> => { const response = await apiClient.get('/files/component-files', { params, }); return response.data; }; /** * 파일 업로드 및 JSON 데이터 생성 * InteractiveScreenViewer에서 사용할 통합 함수 */ export const uploadFilesAndCreateData = async (files: FileList) => { try { // 1. 파일 업로드 const uploadResponse = await uploadFiles({ files }); if (!uploadResponse.success) { throw new Error(uploadResponse.message); } // 2. JSON 데이터 구조 생성 const fileData = { files: uploadResponse.files.map((file) => ({ id: file.id, name: file.name, size: file.size, type: file.type, extension: file.extension, uploaded_at: file.uploaded_at, last_modified: file.last_modified, server_filename: file.server_filename, // 다운로드에 필요 })), totalCount: uploadResponse.files.length, totalSize: uploadResponse.files.reduce((sum, file) => sum + file.size, 0), lastModified: new Date().toISOString(), }; return { success: true, data: fileData, message: uploadResponse.message, }; } catch (error) { console.error("파일 업로드 및 데이터 생성 오류:", error); throw error; } }; /** * 테이블 연결된 파일 조회 */ export const getLinkedFiles = async ( tableName: string, recordId: string, ): Promise<{ success: boolean; files: any[]; totalCount: number; targetObjid: string; }> => { try { console.log("📎 연결된 파일 조회:", { tableName, recordId }); const response = await apiClient.get(`/files/linked/${tableName}/${recordId}`); console.log("✅ 연결된 파일 조회 성공:", response.data); return response.data; } catch (error) { console.error("연결된 파일 조회 오류:", error); throw new Error("연결된 파일 조회에 실패했습니다."); } }; /** * 파일 미리보기 URL 생성 * 🔑 상대 경로(/api) 대신 API_BASE_URL 사용 (Docker 환경에서 Next.js rewrite 의존 방지) */ export const getFilePreviewUrl = (fileId: string): string => { return `${API_BASE_URL}/files/preview/${fileId}`; }; /** * 파일 다운로드 URL 생성 */ export const getFileDownloadUrl = (fileId: string): string => { const baseUrl = process.env.NEXT_PUBLIC_API_URL || "/api"; return `${baseUrl}/files/download/${fileId}`; }; /** * 직접 파일 경로 URL 생성 (정적 파일 서빙) * 주의: 모듈 로드 시점이 아닌 런타임에 hostname을 확인해야 SSR 문제 방지 */ export const getDirectFileUrl = (filePath: string): string => { // 빈 값 체크 if (!filePath) return ""; // 이미 전체 URL인 경우 그대로 반환 if (filePath.startsWith("http://") || filePath.startsWith("https://")) { return filePath; } // 런타임에 현재 hostname 확인 (SSR 시점이 아닌 클라이언트에서 실행될 때) if (typeof window !== "undefined") { const currentHost = window.location.hostname; // 프로덕션 환경: v1.invyone.com → api.invyone.com if (currentHost === "v1.invyone.com") { return `https://api.invyone.com${filePath}`; } // 로컬 개발환경 if (currentHost === "localhost" || currentHost === "127.0.0.1") { return `http://localhost:8081${filePath}`; } } // SSR 또는 알 수 없는 환경에서는 환경변수 사용 (fallback) // 주의: 프로덕션 URL이 https://api.invyone.com/api 이므로 // 단순 .replace("/api", "")는 호스트명의 /api까지 제거하는 버그 발생 const baseUrl = process.env.NEXT_PUBLIC_API_URL?.replace(/\/api$/, "") || ""; if (baseUrl.startsWith("http://") || baseUrl.startsWith("https://")) { return `${baseUrl}${filePath}`; } // 최종 fallback return 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("대표 파일 설정에 실패했습니다."); } }; /** * 파일 정보 조회 (메타데이터만, objid로 조회) */ export const getFileInfoByObjid = async (objid: string): Promise<{ success: boolean; data?: { objid: string; real_file_name: string; file_size: number; file_ext: string; file_path: string; regdate: string; is_representative: boolean; }; message?: string; }> => { try { const response = await apiClient.get(`/files/info/${objid}`); return response.data; } catch (error) { console.error("파일 정보 조회 오류:", error); return { success: false, message: "파일 정보 조회에 실패했습니다.", }; } };