From 2fb6dd0c321cf0160a9677b78a63c73ecffeaeb0 Mon Sep 17 00:00:00 2001 From: kjs Date: Thu, 5 Feb 2026 10:50:34 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=EA=B8=B0=EB=B3=B8=20=EC=A0=95?= =?UTF-8?q?=EB=A0=AC=20=EC=84=A4=EC=A0=95=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=EB=A1=9C=EA=B9=85=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - V2PropertiesPanel에서 defaultSort를 currentConfig에 추가하여 기본 정렬 정보를 관리하도록 개선하였습니다. - TableListComponent에서 localStorage에서 기본 정렬 설정을 적용하는 로직을 추가하여 사용자 경험을 향상시켰습니다. - TableListConfigPanel에 기본 정렬 설정 UI를 추가하여 사용자가 테이블 로드 시 기본 정렬 순서를 지정할 수 있도록 하였습니다. - 각 컴포넌트에서 상태 변경 시 로깅을 추가하여 디버깅을 용이하게 하였습니다. --- .../screen/panels/V2PropertiesPanel.tsx | 8 +++ .../v2-table-list/TableListComponent.tsx | 18 +++-- .../v2-table-list/TableListConfigPanel.tsx | 65 ++++++++++++++++++- .../components/v2-table-list/types.ts | 6 ++ 4 files changed, 92 insertions(+), 5 deletions(-) diff --git a/frontend/components/screen/panels/V2PropertiesPanel.tsx b/frontend/components/screen/panels/V2PropertiesPanel.tsx index a843d710..8b42d753 100644 --- a/frontend/components/screen/panels/V2PropertiesPanel.tsx +++ b/frontend/components/screen/panels/V2PropertiesPanel.tsx @@ -263,6 +263,7 @@ export const V2PropertiesPanel: React.FC = ({ definitionName: definition.name, hasConfigPanel: !!definition.configPanel, currentConfig, + defaultSort: currentConfig?.defaultSort, // 🔍 defaultSort 확인 }); // 🔧 ConfigPanelWrapper를 인라인 함수 대신 직접 JSX 반환 (리마운트 방지) @@ -1059,8 +1060,15 @@ export const V2PropertiesPanel: React.FC = ({ allComponents={allComponents} // 🆕 연쇄 드롭다운 부모 감지용 currentComponent={selectedComponent} // 🆕 현재 컴포넌트 정보 onChange={(newConfig) => { + console.log("🔧 [V2PropertiesPanel] DynamicConfigPanel onChange:", { + componentId: selectedComponent.id, + newConfigKeys: Object.keys(newConfig), + defaultSort: newConfig.defaultSort, + newConfig, + }); // 개별 속성별로 업데이트하여 다른 속성과의 충돌 방지 Object.entries(newConfig).forEach(([key, value]) => { + console.log(` -> handleUpdate: componentConfig.${key} =`, value); handleUpdate(`componentConfig.${key}`, value); }); }} diff --git a/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx b/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx index 02ef8643..543724b5 100644 --- a/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx @@ -1010,7 +1010,7 @@ export const TableListComponent: React.FC = ({ // unregisterTable 함수는 의존성이 없어 안정적임 ]); - // 🎯 초기 로드 시 localStorage에서 정렬 상태 불러오기 + // 🎯 초기 로드 시 localStorage에서 정렬 상태 불러오기 (없으면 defaultSort 적용) useEffect(() => { if (!tableConfig.selectedTable || !userId || hasInitializedSort.current) return; @@ -1024,12 +1024,21 @@ export const TableListComponent: React.FC = ({ setSortColumn(column); setSortDirection(direction); hasInitializedSort.current = true; + return; } } catch (error) { // 정렬 상태 복원 실패 } } - }, [tableConfig.selectedTable, userId]); + + // localStorage에 저장된 정렬이 없으면 defaultSort 설정 적용 + if (tableConfig.defaultSort?.columnName) { + console.log("📊 기본 정렬 설정 적용:", tableConfig.defaultSort); + setSortColumn(tableConfig.defaultSort.columnName); + setSortDirection(tableConfig.defaultSort.direction || "asc"); + hasInitializedSort.current = true; + } + }, [tableConfig.selectedTable, tableConfig.defaultSort, userId]); // 🆕 초기 로드 시 localStorage에서 컬럼 순서 불러오기 useEffect(() => { @@ -1470,8 +1479,9 @@ export const TableListComponent: React.FC = ({ try { const page = tableConfig.pagination?.currentPage || currentPage; const pageSize = localPageSize; - const sortBy = sortColumn || undefined; - const sortOrder = sortDirection; + // 🆕 sortColumn이 없으면 defaultSort 설정을 fallback으로 사용 + const sortBy = sortColumn || tableConfig.defaultSort?.columnName || undefined; + const sortOrder = sortColumn ? sortDirection : (tableConfig.defaultSort?.direction || sortDirection); const search = searchTerm || undefined; // 🆕 연결 필터 값 가져오기 (분할 패널 내부일 때) diff --git a/frontend/lib/registry/components/v2-table-list/TableListConfigPanel.tsx b/frontend/lib/registry/components/v2-table-list/TableListConfigPanel.tsx index ff76960e..977830ca 100644 --- a/frontend/lib/registry/components/v2-table-list/TableListConfigPanel.tsx +++ b/frontend/lib/registry/components/v2-table-list/TableListConfigPanel.tsx @@ -319,7 +319,9 @@ export const TableListConfigPanel: React.FC = ({ const handleChange = (key: keyof TableListConfig, value: any) => { // 기존 config와 병합하여 전달 (다른 속성 손실 방지) - onChange({ ...config, [key]: value }); + const newConfig = { ...config, [key]: value }; + console.log("📊 TableListConfigPanel handleChange:", { key, value, newConfig }); + onChange(newConfig); }; const handleNestedChange = (parentKey: keyof TableListConfig, childKey: string, value: any) => { @@ -884,6 +886,67 @@ export const TableListConfigPanel: React.FC = ({ + {/* 기본 정렬 설정 */} +
+
+

기본 정렬 설정

+

테이블 로드 시 기본 정렬 순서를 지정합니다

+
+
+
+
+ + +
+ + {config.defaultSort?.columnName && ( +
+ + +
+ )} +
+
+ {/* 가로 스크롤 및 컬럼 고정 */}
diff --git a/frontend/lib/registry/components/v2-table-list/types.ts b/frontend/lib/registry/components/v2-table-list/types.ts index a43ccdfa..1cc04375 100644 --- a/frontend/lib/registry/components/v2-table-list/types.ts +++ b/frontend/lib/registry/components/v2-table-list/types.ts @@ -278,6 +278,12 @@ export interface TableListConfig extends ComponentConfig { autoLoad: boolean; refreshInterval?: number; // 초 단위 + // 🆕 기본 정렬 설정 + defaultSort?: { + columnName: string; // 정렬할 컬럼명 + direction: "asc" | "desc"; // 정렬 방향 + }; + // 🆕 툴바 버튼 표시 설정 toolbar?: ToolbarConfig; From 21a663a99b87de863ab8e015c04d330d86f267f6 Mon Sep 17 00:00:00 2001 From: kjs Date: Thu, 5 Feb 2026 10:58:27 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=ED=85=8C=EC=9D=B4=EB=B8=94=20?= =?UTF-8?q?=EC=B0=B8=EC=A1=B0=20=EA=B4=80=EA=B3=84=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20=EB=A1=9C?= =?UTF-8?q?=EA=B9=85=20=EA=B0=95=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - getReferencedByTables 함수에서 현재 사용자의 company_code를 우선적으로 사용하도록 로직을 개선하였습니다. - SQL 쿼리에서 ROW_NUMBER를 활용하여 같은 테이블/컬럼 조합에서 회사코드 우선순위로 하나만 선택하도록 하였습니다. - 로깅 메시지에 회사코드를 포함시켜, 조회 시 어떤 회사의 데이터가 사용되었는지 명확히 기록하도록 하였습니다. --- .../controllers/tableManagementController.ts | 52 +++++++++++++------ 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/backend-node/src/controllers/tableManagementController.ts b/backend-node/src/controllers/tableManagementController.ts index 4fa08eed..a494ae3d 100644 --- a/backend-node/src/controllers/tableManagementController.ts +++ b/backend-node/src/controllers/tableManagementController.ts @@ -2344,6 +2344,8 @@ export async function getTableEntityRelations( * * table_type_columns에서 reference_table이 현재 테이블인 레코드를 찾아서 * 해당 테이블과 FK 컬럼 정보를 반환합니다. + * + * 우선순위: 현재 사용자의 company_code > 공통('*') */ export async function getReferencedByTables( req: AuthenticatedRequest, @@ -2351,9 +2353,11 @@ export async function getReferencedByTables( ): Promise { try { const { tableName } = req.params; + // 현재 사용자의 회사 코드 (없으면 '*' 사용) + const userCompanyCode = req.user?.companyCode || "*"; logger.info( - `=== 테이블 참조 관계 조회 시작: ${tableName} 을 참조하는 테이블 ===` + `=== 테이블 참조 관계 조회 시작: ${tableName} 을 참조하는 테이블 (회사코드: ${userCompanyCode}) ===` ); if (!tableName) { @@ -2371,23 +2375,41 @@ export async function getReferencedByTables( // table_type_columns에서 reference_table이 현재 테이블인 레코드 조회 // input_type이 'entity'인 것만 조회 (실제 FK 관계) + // 우선순위: 현재 사용자의 company_code > 공통('*') + // ROW_NUMBER를 사용해서 같은 테이블/컬럼 조합에서 회사코드 우선순위로 하나만 선택 const sqlQuery = ` + WITH ranked AS ( + SELECT + ttc.table_name, + ttc.column_name, + ttc.column_label, + ttc.reference_table, + ttc.reference_column, + ttc.display_column, + ttc.company_code, + ROW_NUMBER() OVER ( + PARTITION BY ttc.table_name, ttc.column_name + ORDER BY CASE WHEN ttc.company_code = $2 THEN 1 ELSE 2 END + ) as rn + FROM table_type_columns ttc + WHERE ttc.reference_table = $1 + AND ttc.input_type = 'entity' + AND ttc.company_code IN ($2, '*') + ) SELECT DISTINCT - ttc.table_name, - ttc.column_name, - ttc.column_label, - ttc.reference_table, - ttc.reference_column, - ttc.display_column, - ttc.table_name as table_label - FROM table_type_columns ttc - WHERE ttc.reference_table = $1 - AND ttc.input_type = 'entity' - AND ttc.company_code = '*' - ORDER BY ttc.table_name, ttc.column_name + table_name, + column_name, + column_label, + reference_table, + reference_column, + display_column, + table_name as table_label + FROM ranked + WHERE rn = 1 + ORDER BY table_name, column_name `; - const result = await query(sqlQuery, [tableName]); + const result = await query(sqlQuery, [tableName, userCompanyCode]); const referencedByTables = result.map((row: any) => ({ tableName: row.table_name, @@ -2400,7 +2422,7 @@ export async function getReferencedByTables( })); logger.info( - `테이블 참조 관계 조회 완료: ${referencedByTables.length}개 발견` + `테이블 참조 관계 조회 완료: ${referencedByTables.length}개 발견 (회사코드: ${userCompanyCode})` ); const response: ApiResponse = { From ad7c5923a65a2109aa6e7ce14012229a8dc2ea11 Mon Sep 17 00:00:00 2001 From: kjs Date: Thu, 5 Feb 2026 13:45:23 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20=ED=8C=8C=EC=9D=BC=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20API=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20=ED=8C=8C=EC=9D=BC=20=EC=97=85=EB=A1=9C=EB=93=9C=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 파일 정보 조회를 위한 getFileInfo 함수를 추가하여, 파일의 메타데이터를 공개 접근으로 조회할 수 있도록 하였습니다. - 파일 업로드 컴포넌트에서 파일 아이콘 매핑 및 파일 미리보기 기능을 개선하여 사용자 경험을 향상시켰습니다. - V2 파일 업로드 컴포넌트의 설정 패널을 추가하여, 파일 업로드 관련 설정을 보다 쉽게 관리할 수 있도록 하였습니다. - 파일 뷰어 모달을 추가하여 다양한 파일 형식의 미리보기를 지원합니다. --- .../src/controllers/fileController.ts | 51 + backend-node/src/routes/fileRoutes.ts | 8 + .../screen/InteractiveDataTable.tsx | 33 +- frontend/components/v2/V2Media.tsx | 22 +- frontend/lib/api/file.ts | 28 + .../registry/components/file-upload/index.ts | 9 +- .../registry/components/image-widget/index.ts | 7 +- frontend/lib/registry/components/index.ts | 1 + .../v2-file-upload/FileManagerModal.tsx | 421 ++++++ .../v2-file-upload/FileUploadComponent.tsx | 1345 +++++++++++++++++ .../v2-file-upload/FileUploadConfigPanel.tsx | 287 ++++ .../v2-file-upload/FileViewerModal.tsx | 543 +++++++ .../v2-file-upload/V2FileUploadRenderer.tsx | 56 + .../components/v2-file-upload/config.ts | 62 + .../components/v2-file-upload/index.ts | 46 + .../components/v2-file-upload/types.ts | 114 ++ .../components/v2-media/V2MediaRenderer.tsx | 26 +- .../v2-table-list/TableListComponent.tsx | 67 +- frontend/lib/utils/webTypeMapping.ts | 20 +- 19 files changed, 3103 insertions(+), 43 deletions(-) create mode 100644 frontend/lib/registry/components/v2-file-upload/FileManagerModal.tsx create mode 100644 frontend/lib/registry/components/v2-file-upload/FileUploadComponent.tsx create mode 100644 frontend/lib/registry/components/v2-file-upload/FileUploadConfigPanel.tsx create mode 100644 frontend/lib/registry/components/v2-file-upload/FileViewerModal.tsx create mode 100644 frontend/lib/registry/components/v2-file-upload/V2FileUploadRenderer.tsx create mode 100644 frontend/lib/registry/components/v2-file-upload/config.ts create mode 100644 frontend/lib/registry/components/v2-file-upload/index.ts create mode 100644 frontend/lib/registry/components/v2-file-upload/types.ts diff --git a/backend-node/src/controllers/fileController.ts b/backend-node/src/controllers/fileController.ts index a648a4f9..28a46232 100644 --- a/backend-node/src/controllers/fileController.ts +++ b/backend-node/src/controllers/fileController.ts @@ -1261,5 +1261,56 @@ export const setRepresentativeFile = async ( } }; +/** + * 파일 정보 조회 (메타데이터만, 파일 내용 없음) + * 공개 접근 허용 + */ +export const getFileInfo = async (req: Request, res: Response) => { + try { + const { objid } = req.params; + + if (!objid) { + return res.status(400).json({ + success: false, + message: "파일 ID가 필요합니다.", + }); + } + + // 파일 정보 조회 + const fileRecord = await queryOne( + `SELECT objid, real_file_name, file_size, file_ext, file_path, regdate, is_representative + FROM attach_file_info + WHERE objid = $1 AND status = 'ACTIVE'`, + [parseInt(objid)] + ); + + if (!fileRecord) { + return res.status(404).json({ + success: false, + message: "파일을 찾을 수 없습니다.", + }); + } + + res.json({ + success: true, + data: { + objid: fileRecord.objid.toString(), + realFileName: fileRecord.real_file_name, + fileSize: fileRecord.file_size, + fileExt: fileRecord.file_ext, + filePath: fileRecord.file_path, + regdate: fileRecord.regdate, + isRepresentative: fileRecord.is_representative, + }, + }); + } 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 4514e37f..562a0b7f 100644 --- a/backend-node/src/routes/fileRoutes.ts +++ b/backend-node/src/routes/fileRoutes.ts @@ -11,6 +11,7 @@ import { generateTempToken, getFileByToken, setRepresentativeFile, + getFileInfo, } from "../controllers/fileController"; import { authenticateToken } from "../middleware/authMiddleware"; @@ -31,6 +32,13 @@ router.get("/public/:token", getFileByToken); */ router.get("/preview/:objid", previewFile); +/** + * @route GET /api/files/info/:objid + * @desc 파일 정보 조회 (메타데이터만, 파일 내용 없음) - 공개 접근 허용 + * @access Public + */ +router.get("/info/:objid", getFileInfo); + // 모든 파일 API는 인증 필요 router.use(authenticateToken); diff --git a/frontend/components/screen/InteractiveDataTable.tsx b/frontend/components/screen/InteractiveDataTable.tsx index 2c400df5..582aa413 100644 --- a/frontend/components/screen/InteractiveDataTable.tsx +++ b/frontend/components/screen/InteractiveDataTable.tsx @@ -43,7 +43,7 @@ import { } from "lucide-react"; import { tableTypeApi } from "@/lib/api/screen"; import { commonCodeApi } from "@/lib/api/commonCode"; -import { apiClient, getCurrentUser, UserInfo } from "@/lib/api/client"; +import { apiClient, getCurrentUser, UserInfo, getFullImageUrl } from "@/lib/api/client"; import { useAuth } from "@/hooks/useAuth"; import { DataTableComponent, DataTableColumn, DataTableFilter } from "@/types/screen-legacy-backup"; import { cn } from "@/lib/utils"; @@ -2224,6 +2224,37 @@ export const InteractiveDataTable: React.FC = ({ // 파일 타입 컬럼 처리 (가상 파일 컬럼 포함) const isFileColumn = actualWebType === "file" || column.columnName === "file_path" || column.isVirtualFileColumn; + // 🖼️ 이미지 타입 컬럼: 썸네일로 표시 + const isImageColumn = actualWebType === "image" || actualWebType === "img"; + if (isImageColumn && value) { + // value가 objid (숫자 또는 숫자 문자열)인 경우 파일 API URL 사용 + // 🔑 download 대신 preview 사용 (공개 접근 허용) + const isObjid = /^\d+$/.test(String(value)); + const imageUrl = isObjid + ? `/api/files/preview/${value}` + : getFullImageUrl(String(value)); + + return ( +
+ 이미지 { + e.stopPropagation(); + // 이미지 클릭 시 크게 보기 (새 탭에서 열기) + window.open(imageUrl, "_blank"); + }} + onError={(e) => { + // 이미지 로드 실패 시 기본 아이콘 표시 + (e.target as HTMLImageElement).style.display = "none"; + }} + /> +
+ ); + } + // 파일 타입 컬럼은 파일 아이콘으로 표시 (컬럼별 파일 관리) if (isFileColumn && rowData) { // 현재 행의 기본키 값 가져오기 diff --git a/frontend/components/v2/V2Media.tsx b/frontend/components/v2/V2Media.tsx index 7321808f..733d6657 100644 --- a/frontend/components/v2/V2Media.tsx +++ b/frontend/components/v2/V2Media.tsx @@ -124,7 +124,8 @@ export const V2Media = forwardRef( const isRecordMode = !!(formData?.id && !String(formData.id).startsWith('temp_')); const recordTableName = formData?.tableName || tableName; const recordId = formData?.id; - const effectiveColumnName = isRecordMode ? 'attachments' : (columnName || id || 'attachments'); + // 🔑 columnName 우선 사용 (실제 DB 컬럼명), 없으면 id, 최후에 attachments + const effectiveColumnName = columnName || id || 'attachments'; // 레코드용 targetObjid 생성 const getRecordTargetObjid = useCallback(() => { @@ -471,13 +472,21 @@ export const V2Media = forwardRef( // 폼 데이터 업데이트 - 부모 컴포넌트 시그니처에 맞게 (fieldName, value) 형식 if (onFormDataChange && targetColumn) { + // 🔑 단일 파일: 첫 번째 objid만 전달 (DB 컬럼에 저장될 값) + // 복수 파일: 콤마 구분 문자열로 전달 + const formValue = config.multiple + ? fileIds.join(',') + : (fileIds[0] || ''); + console.log("📝 [V2Media] formData 업데이트:", { columnName: targetColumn, fileIds, + formValue, + isMultiple: config.multiple, isRecordMode: effectiveIsRecordMode, }); // (fieldName: string, value: any) 형식으로 호출 - onFormDataChange(targetColumn, fileIds); + onFormDataChange(targetColumn, formValue); } // 그리드 파일 상태 새로고침 이벤트 발생 @@ -601,12 +610,19 @@ export const V2Media = forwardRef( // 폼 데이터 업데이트 - 부모 컴포넌트 시그니처에 맞게 (fieldName, value) 형식 if (onFormDataChange && targetColumn) { + // 🔑 단일 파일: 첫 번째 objid만 전달 (DB 컬럼에 저장될 값) + // 복수 파일: 콤마 구분 문자열로 전달 + const formValue = config.multiple + ? fileIds.join(',') + : (fileIds[0] || ''); + console.log("🗑️ [V2Media] 삭제 후 formData 업데이트:", { columnName: targetColumn, fileIds, + formValue, }); // (fieldName: string, value: any) 형식으로 호출 - onFormDataChange(targetColumn, fileIds); + onFormDataChange(targetColumn, formValue); } toast.success(`${fileName} 삭제 완료`); diff --git a/frontend/lib/api/file.ts b/frontend/lib/api/file.ts index e6cab8ae..f848c7e6 100644 --- a/frontend/lib/api/file.ts +++ b/frontend/lib/api/file.ts @@ -298,3 +298,31 @@ export const setRepresentativeFile = async (objid: string): Promise<{ throw new Error("대표 파일 설정에 실패했습니다."); } }; + +/** + * 파일 정보 조회 (메타데이터만, objid로 조회) + */ +export const getFileInfoByObjid = async (objid: string): Promise<{ + success: boolean; + data?: { + objid: string; + realFileName: string; + fileSize: number; + fileExt: string; + filePath: string; + regdate: string; + isRepresentative: boolean; + }; + message?: string; +}> => { + try { + const response = await apiClient.get(`/files/info/${objid}`); + return response.data; + } catch (error) { + console.error("파일 정보 조회 오류:", error); + return { + success: false, + message: "파일 정보 조회에 실패했습니다.", + }; + } +}; diff --git a/frontend/lib/registry/components/file-upload/index.ts b/frontend/lib/registry/components/file-upload/index.ts index fcca65cc..3f059ae1 100644 --- a/frontend/lib/registry/components/file-upload/index.ts +++ b/frontend/lib/registry/components/file-upload/index.ts @@ -14,22 +14,23 @@ import { FileUploadConfig } from "./types"; */ export const FileUploadDefinition = createComponentDefinition({ id: "file-upload", - name: "파일 업로드", - nameEng: "FileUpload Component", - description: "파일 업로드를 위한 파일 선택 컴포넌트", + name: "파일 업로드 (레거시)", + nameEng: "FileUpload Component (Legacy)", + description: "파일 업로드를 위한 파일 선택 컴포넌트 (레거시)", category: ComponentCategory.INPUT, webType: "file", component: FileUploadComponent, defaultConfig: { placeholder: "입력하세요", }, - defaultSize: { width: 350, height: 240 }, // 40 * 6 (파일 선택 + 목록 표시) + defaultSize: { width: 350, height: 240 }, configPanel: FileUploadConfigPanel, icon: "Edit", tags: [], version: "1.0.0", author: "개발팀", documentation: "https://docs.example.com/components/file-upload", + hidden: true, // v2-file-upload 사용으로 패널에서 숨김 }); // 타입 내보내기 diff --git a/frontend/lib/registry/components/image-widget/index.ts b/frontend/lib/registry/components/image-widget/index.ts index 67abbc80..aee663e8 100644 --- a/frontend/lib/registry/components/image-widget/index.ts +++ b/frontend/lib/registry/components/image-widget/index.ts @@ -13,9 +13,9 @@ import { ImageWidgetConfigPanel } from "./ImageWidgetConfigPanel"; */ export const ImageWidgetDefinition = createComponentDefinition({ id: "image-widget", - name: "이미지 위젯", - nameEng: "Image Widget", - description: "이미지 표시 및 업로드", + name: "이미지 위젯 (레거시)", + nameEng: "Image Widget (Legacy)", + description: "이미지 표시 및 업로드 (레거시)", category: ComponentCategory.INPUT, webType: "image", component: ImageWidget, @@ -32,6 +32,7 @@ export const ImageWidgetDefinition = createComponentDefinition({ version: "1.0.0", author: "개발팀", documentation: "https://docs.example.com/components/image-widget", + hidden: true, // v2-file-upload 사용으로 패널에서 숨김 }); // 컴포넌트 내보내기 diff --git a/frontend/lib/registry/components/index.ts b/frontend/lib/registry/components/index.ts index 6519230d..172f0067 100644 --- a/frontend/lib/registry/components/index.ts +++ b/frontend/lib/registry/components/index.ts @@ -111,6 +111,7 @@ import "./v2-timeline-scheduler/TimelineSchedulerRenderer"; // 타임라인 스 import "./v2-input/V2InputRenderer"; // V2 통합 입력 컴포넌트 import "./v2-select/V2SelectRenderer"; // V2 통합 선택 컴포넌트 import "./v2-date/V2DateRenderer"; // V2 통합 날짜 컴포넌트 +import "./v2-file-upload/V2FileUploadRenderer"; // V2 파일 업로드 컴포넌트 /** * 컴포넌트 초기화 함수 diff --git a/frontend/lib/registry/components/v2-file-upload/FileManagerModal.tsx b/frontend/lib/registry/components/v2-file-upload/FileManagerModal.tsx new file mode 100644 index 00000000..de838fbf --- /dev/null +++ b/frontend/lib/registry/components/v2-file-upload/FileManagerModal.tsx @@ -0,0 +1,421 @@ +"use client"; + +import React, { useState, useRef } from "react"; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { FileInfo, FileUploadConfig } from "./types"; +import { + Upload, + Download, + Trash2, + Eye, + File, + FileText, + Image as ImageIcon, + Video, + Music, + Archive, + Presentation, + X, + Star +} from "lucide-react"; +import { formatFileSize } from "@/lib/utils"; +import { FileViewerModal } from "./FileViewerModal"; + +interface FileManagerModalProps { + isOpen: boolean; + onClose: () => void; + uploadedFiles: FileInfo[]; + onFileUpload: (files: File[]) => Promise; + onFileDownload: (file: FileInfo) => void; + onFileDelete: (file: FileInfo) => void; + onFileView: (file: FileInfo) => void; + onSetRepresentative?: (file: FileInfo) => void; // 대표 이미지 설정 콜백 + config: FileUploadConfig; + isDesignMode?: boolean; +} + +export const FileManagerModal: React.FC = ({ + isOpen, + onClose, + uploadedFiles, + onFileUpload, + onFileDownload, + onFileDelete, + onFileView, + onSetRepresentative, + config, + isDesignMode = false, +}) => { + const [dragOver, setDragOver] = useState(false); + const [uploading, setUploading] = useState(false); + const [viewerFile, setViewerFile] = useState(null); + const [isViewerOpen, setIsViewerOpen] = useState(false); + const [selectedFile, setSelectedFile] = useState(null); // 선택된 파일 (좌측 미리보기용) + const [previewImageUrl, setPreviewImageUrl] = useState(null); // 이미지 미리보기 URL + const fileInputRef = useRef(null); + + // 파일 아이콘 가져오기 + const getFileIcon = (fileExt: string) => { + const ext = fileExt.toLowerCase(); + + if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(ext)) { + return ; + } else if (['pdf', 'doc', 'docx', 'txt', 'rtf'].includes(ext)) { + return ; + } else if (['xls', 'xlsx', 'csv'].includes(ext)) { + return ; + } else if (['ppt', 'pptx'].includes(ext)) { + return ; + } else if (['mp4', 'avi', 'mov', 'webm'].includes(ext)) { + return
)} - {/* 좌우 분할 레이아웃 */} + {/* 좌우 분할 레이아웃 - 좌측 넓게, 우측 고정 너비 */}
- {/* 좌측: 이미지 미리보기 */} -
- {selectedFile && previewImageUrl ? ( - {selectedFile.realFileName} - ) : selectedFile ? ( -
- {getFileIcon(selectedFile.fileExt)} -

미리보기 불가능

+ {/* 좌측: 이미지 미리보기 (확대/축소 가능) */} +
+ {/* 확대/축소 컨트롤 */} + {selectedFile && previewImageUrl && ( +
+ + + {Math.round(zoomLevel * 100)}% + + +
- ) : ( -
- -

파일을 선택하면 미리보기가 표시됩니다

+ )} + + {/* 이미지 미리보기 영역 - 마우스 휠로 확대/축소, 드래그로 이동 */} +
1 ? (isDragging ? 'cursor-grabbing' : 'cursor-grab') : 'cursor-zoom-in' + }`} + onWheel={(e) => { + if (selectedFile && previewImageUrl) { + e.preventDefault(); + const delta = e.deltaY > 0 ? -0.1 : 0.1; + setZoomLevel(prev => Math.min(4, Math.max(0.25, prev + delta))); + } + }} + onMouseDown={handleMouseDown} + onMouseMove={handleMouseMove} + onMouseUp={handleMouseUp} + onMouseLeave={handleMouseUp} + > + {selectedFile && previewImageUrl ? ( + {selectedFile.realFileName} + ) : selectedFile ? ( +
+ {getFileIcon(selectedFile.fileExt)} +

미리보기 불가능

+
+ ) : ( +
+ +

파일을 선택하면 미리보기가 표시됩니다

+
+ )} +
+ + {/* 파일 정보 바 */} + {selectedFile && ( +
+ {selectedFile.realFileName}
)}
- {/* 우측: 파일 목록 */} -
+ {/* 우측: 파일 목록 (고정 너비) */} +