화면관리 쪽 파일첨부 수정
This commit is contained in:
parent
1fe401c7d6
commit
3600621554
|
|
@ -32,6 +32,7 @@ import dataRoutes from "./routes/dataRoutes";
|
|||
import testButtonDataflowRoutes from "./routes/testButtonDataflowRoutes";
|
||||
import externalDbConnectionRoutes from "./routes/externalDbConnectionRoutes";
|
||||
import multiConnectionRoutes from "./routes/multiConnectionRoutes";
|
||||
import screenFileRoutes from "./routes/screenFileRoutes";
|
||||
import dbTypeCategoryRoutes from "./routes/dbTypeCategoryRoutes";
|
||||
import ddlRoutes from "./routes/ddlRoutes";
|
||||
import entityReferenceRoutes from "./routes/entityReferenceRoutes";
|
||||
|
|
@ -132,6 +133,7 @@ app.use("/api/data", dataRoutes);
|
|||
app.use("/api/test-button-dataflow", testButtonDataflowRoutes);
|
||||
app.use("/api/external-db-connections", externalDbConnectionRoutes);
|
||||
app.use("/api/multi-connection", multiConnectionRoutes);
|
||||
app.use("/api/screen-files", screenFileRoutes);
|
||||
app.use("/api/db-type-categories", dbTypeCategoryRoutes);
|
||||
app.use("/api/ddl", ddlRoutes);
|
||||
app.use("/api/entity-reference", entityReferenceRoutes);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,145 @@
|
|||
import { Request, Response } from 'express';
|
||||
import { AuthenticatedRequest } from '../middleware/authMiddleware';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import logger from '../utils/logger';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
/**
|
||||
* 화면 컴포넌트별 파일 정보 조회 및 복원
|
||||
*/
|
||||
export const getScreenComponentFiles = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const { screenId } = req.params;
|
||||
|
||||
logger.info(`화면 컴포넌트 파일 조회 시작: screenId=${screenId}`);
|
||||
|
||||
// screen_files: 접두사로 해당 화면의 모든 파일 조회
|
||||
const targetObjidPattern = `screen_files:${screenId}:%`;
|
||||
|
||||
const files = await prisma.attach_file_info.findMany({
|
||||
where: {
|
||||
target_objid: {
|
||||
startsWith: `screen_files:${screenId}:`
|
||||
},
|
||||
status: 'ACTIVE'
|
||||
},
|
||||
orderBy: {
|
||||
regdate: 'desc'
|
||||
}
|
||||
});
|
||||
|
||||
// 컴포넌트별로 파일 그룹화
|
||||
const componentFiles: { [componentId: string]: any[] } = {};
|
||||
|
||||
files.forEach(file => {
|
||||
// target_objid 형식: screen_files:screenId:componentId:fieldName
|
||||
const targetParts = file.target_objid?.split(':') || [];
|
||||
if (targetParts.length >= 3) {
|
||||
const componentId = targetParts[2];
|
||||
|
||||
if (!componentFiles[componentId]) {
|
||||
componentFiles[componentId] = [];
|
||||
}
|
||||
|
||||
componentFiles[componentId].push({
|
||||
objid: file.objid.toString(),
|
||||
savedFileName: file.saved_file_name,
|
||||
realFileName: file.real_file_name,
|
||||
fileSize: Number(file.file_size),
|
||||
fileExt: file.file_ext,
|
||||
filePath: file.file_path,
|
||||
docType: file.doc_type,
|
||||
docTypeName: file.doc_type_name,
|
||||
targetObjid: file.target_objid,
|
||||
parentTargetObjid: file.parent_target_objid,
|
||||
writer: file.writer,
|
||||
regdate: file.regdate?.toISOString(),
|
||||
status: file.status
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
logger.info(`화면 컴포넌트 파일 조회 완료: ${Object.keys(componentFiles).length}개 컴포넌트, 총 ${files.length}개 파일`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
componentFiles: componentFiles,
|
||||
totalFiles: files.length,
|
||||
componentCount: Object.keys(componentFiles).length
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('화면 컴포넌트 파일 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '화면 컴포넌트 파일 조회 중 오류가 발생했습니다.',
|
||||
error: error instanceof Error ? error.message : '알 수 없는 오류'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 특정 컴포넌트의 파일 목록 조회
|
||||
*/
|
||||
export const getComponentFiles = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const { screenId, componentId } = req.params;
|
||||
|
||||
logger.info(`컴포넌트 파일 조회: screenId=${screenId}, componentId=${componentId}`);
|
||||
|
||||
// target_objid 패턴: screen_files:screenId:componentId:*
|
||||
const targetObjidPattern = `screen_files:${screenId}:${componentId}:`;
|
||||
|
||||
const files = await prisma.attach_file_info.findMany({
|
||||
where: {
|
||||
target_objid: {
|
||||
startsWith: targetObjidPattern
|
||||
},
|
||||
status: 'ACTIVE'
|
||||
},
|
||||
orderBy: {
|
||||
regdate: 'desc'
|
||||
}
|
||||
});
|
||||
|
||||
const fileList = files.map(file => ({
|
||||
objid: file.objid.toString(),
|
||||
savedFileName: file.saved_file_name,
|
||||
realFileName: file.real_file_name,
|
||||
fileSize: Number(file.file_size),
|
||||
fileExt: file.file_ext,
|
||||
filePath: file.file_path,
|
||||
docType: file.doc_type,
|
||||
docTypeName: file.doc_type_name,
|
||||
targetObjid: file.target_objid,
|
||||
parentTargetObjid: file.parent_target_objid,
|
||||
writer: file.writer,
|
||||
regdate: file.regdate?.toISOString(),
|
||||
status: file.status
|
||||
}));
|
||||
|
||||
logger.info(`컴포넌트 파일 조회 완료: ${fileList.length}개 파일`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
files: fileList,
|
||||
componentId: componentId,
|
||||
screenId: screenId
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('컴포넌트 파일 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '컴포넌트 파일 조회 중 오류가 발생했습니다.',
|
||||
error: error instanceof Error ? error.message : '알 수 없는 오류'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { Router } from 'express';
|
||||
import { authenticateToken } from '../middleware/authMiddleware';
|
||||
import { getScreenComponentFiles, getComponentFiles } from '../controllers/screenFileController';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// 화면 컴포넌트별 파일 정보 조회
|
||||
router.get('/screens/:screenId/components/files', authenticateToken, getScreenComponentFiles);
|
||||
|
||||
// 특정 컴포넌트의 파일 목록 조회
|
||||
router.get('/screens/:screenId/components/:componentId/files', authenticateToken, getComponentFiles);
|
||||
|
||||
export default router;
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { LAYOUT_CONFIG } from "@/constants/layout";
|
||||
import Image from "next/image";
|
||||
|
||||
/**
|
||||
* 로고 컴포넌트
|
||||
|
|
@ -6,10 +7,17 @@ import { LAYOUT_CONFIG } from "@/constants/layout";
|
|||
export function Logo() {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="bg-primary flex h-8 w-8 items-center justify-center rounded-lg">
|
||||
<span className="text-primary-foreground text-sm font-bold">P</span>
|
||||
<div className="flex items-center justify-center">
|
||||
<Image
|
||||
src="/images/vexplor.png"
|
||||
alt="WACE 솔루션 로고"
|
||||
width={120}
|
||||
height={32}
|
||||
className="h-8 object-contain"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
<span className="font-semibold">{LAYOUT_CONFIG.COMPANY_NAME}</span>
|
||||
{/* <span className="font-semibold">{LAYOUT_CONFIG.COMPANY_NAME}</span> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -509,6 +509,48 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
|||
fetchCurrentUser();
|
||||
}, []);
|
||||
|
||||
// 파일 상태 새로고침 이벤트 리스너
|
||||
useEffect(() => {
|
||||
const handleRefreshFileStatus = async (event: CustomEvent) => {
|
||||
const { tableName, recordId, columnName, targetObjid, fileCount } = event.detail;
|
||||
|
||||
console.log("🔄 InteractiveDataTable 파일 상태 새로고침 이벤트 수신:", {
|
||||
tableName,
|
||||
recordId,
|
||||
columnName,
|
||||
targetObjid,
|
||||
fileCount,
|
||||
currentTableName: component.tableName
|
||||
});
|
||||
|
||||
// 현재 테이블과 일치하는지 확인
|
||||
if (tableName === component.tableName) {
|
||||
// 해당 행의 파일 상태 업데이트
|
||||
const columnKey = `${recordId}_${columnName}`;
|
||||
setFileStatusMap(prev => ({
|
||||
...prev,
|
||||
[recordId]: { hasFiles: fileCount > 0, fileCount },
|
||||
[columnKey]: { hasFiles: fileCount > 0, fileCount }
|
||||
}));
|
||||
|
||||
console.log("✅ 파일 상태 업데이트 완료:", {
|
||||
recordId,
|
||||
columnKey,
|
||||
hasFiles: fileCount > 0,
|
||||
fileCount
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('refreshFileStatus', handleRefreshFileStatus as EventListener);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('refreshFileStatus', handleRefreshFileStatus as EventListener);
|
||||
};
|
||||
}
|
||||
}, [component.tableName]);
|
||||
|
||||
// 테이블 컬럼 정보 로드 (웹 타입 정보 포함)
|
||||
useEffect(() => {
|
||||
const fetchTableColumns = async () => {
|
||||
|
|
|
|||
|
|
@ -417,24 +417,40 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
|||
<div className="h-full w-full">
|
||||
{/* 실제 FileUploadComponent 사용 */}
|
||||
<FileUploadComponent
|
||||
component={{
|
||||
...comp,
|
||||
config: {
|
||||
...comp.fileConfig,
|
||||
multiple: comp.fileConfig?.multiple !== false,
|
||||
accept: comp.fileConfig?.accept || "*/*",
|
||||
maxSize: (comp.fileConfig?.maxSize || 10) * 1024 * 1024, // MB to bytes
|
||||
disabled: readonly,
|
||||
}
|
||||
component={comp}
|
||||
componentConfig={{
|
||||
...comp.fileConfig,
|
||||
multiple: comp.fileConfig?.multiple !== false,
|
||||
accept: comp.fileConfig?.accept || "*/*",
|
||||
maxSize: (comp.fileConfig?.maxSize || 10) * 1024 * 1024, // MB to bytes
|
||||
disabled: readonly,
|
||||
}}
|
||||
componentStyle={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
className="h-full w-full"
|
||||
isInteractive={true}
|
||||
isDesignMode={false}
|
||||
formData={{
|
||||
tableName: screenInfo?.tableName,
|
||||
id: formData.id,
|
||||
...formData
|
||||
}}
|
||||
onFormDataChange={(fieldName, value) => {
|
||||
console.log("📝 파일 업로드 완료:", { fieldName, value });
|
||||
handleFormDataChange(fieldName, value);
|
||||
onFormDataChange={(data) => {
|
||||
console.log("📝 파일 업로드 완료:", data);
|
||||
if (onFormDataChange) {
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
onFormDataChange(key, value);
|
||||
});
|
||||
}
|
||||
}}
|
||||
onUpdate={(updates) => {
|
||||
console.log("🔄 파일 컴포넌트 업데이트:", updates);
|
||||
// 파일 업로드 완료 시 formData 업데이트
|
||||
if (updates.uploadedFiles && onFormDataChange) {
|
||||
onFormDataChange(fieldName, updates.uploadedFiles);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ import { toast } from "sonner";
|
|||
import { MenuAssignmentModal } from "./MenuAssignmentModal";
|
||||
import { FileAttachmentDetailModal } from "./FileAttachmentDetailModal";
|
||||
import { initializeComponents } from "@/lib/registry/components";
|
||||
import { ScreenFileAPI } from "@/lib/api/screenFile";
|
||||
|
||||
import StyleEditor from "./StyleEditor";
|
||||
import { RealtimePreview } from "./RealtimePreviewDynamic";
|
||||
|
|
@ -196,6 +197,85 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
// 전역 파일 상태 변경 시 강제 리렌더링을 위한 상태
|
||||
const [forceRenderTrigger, setForceRenderTrigger] = useState(0);
|
||||
|
||||
// 파일 컴포넌트 데이터 복원 함수 (실제 DB에서 조회)
|
||||
const restoreFileComponentsData = useCallback(async (components: ComponentData[]) => {
|
||||
if (!selectedScreen?.screenId) return;
|
||||
|
||||
console.log("🔄 파일 컴포넌트 데이터 복원 시작:", components.length);
|
||||
|
||||
try {
|
||||
// 실제 DB에서 화면의 모든 파일 정보 조회
|
||||
const fileResponse = await ScreenFileAPI.getScreenComponentFiles(selectedScreen.screenId);
|
||||
|
||||
if (!fileResponse.success) {
|
||||
console.warn("⚠️ 파일 정보 조회 실패:", fileResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
const { componentFiles } = fileResponse;
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
// 전역 파일 상태 초기화
|
||||
const globalFileState: {[key: string]: any[]} = {};
|
||||
let restoredCount = 0;
|
||||
|
||||
// DB에서 조회한 파일 정보를 전역 상태로 복원
|
||||
Object.keys(componentFiles).forEach(componentId => {
|
||||
const files = componentFiles[componentId];
|
||||
if (files && files.length > 0) {
|
||||
globalFileState[componentId] = files;
|
||||
restoredCount++;
|
||||
|
||||
// localStorage에도 백업
|
||||
const backupKey = `fileComponent_${componentId}_files`;
|
||||
localStorage.setItem(backupKey, JSON.stringify(files));
|
||||
|
||||
console.log("📁 DB에서 파일 컴포넌트 데이터 복원:", {
|
||||
componentId: componentId,
|
||||
fileCount: files.length,
|
||||
files: files.map(f => ({ objid: f.objid, name: f.realFileName }))
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 전역 상태 업데이트
|
||||
(window as any).globalFileState = globalFileState;
|
||||
|
||||
// 모든 파일 컴포넌트에 복원 완료 이벤트 발생
|
||||
Object.keys(globalFileState).forEach(componentId => {
|
||||
const files = globalFileState[componentId];
|
||||
const syncEvent = new CustomEvent('globalFileStateChanged', {
|
||||
detail: {
|
||||
componentId: componentId,
|
||||
files: files,
|
||||
fileCount: files.length,
|
||||
timestamp: Date.now(),
|
||||
isRestore: true
|
||||
}
|
||||
});
|
||||
window.dispatchEvent(syncEvent);
|
||||
});
|
||||
|
||||
console.log("✅ DB 파일 컴포넌트 데이터 복원 완료:", {
|
||||
totalComponents: components.length,
|
||||
restoredFileComponents: restoredCount,
|
||||
totalFiles: fileResponse.totalFiles,
|
||||
globalFileState: Object.keys(globalFileState).map(id => ({
|
||||
id,
|
||||
fileCount: globalFileState[id]?.length || 0
|
||||
}))
|
||||
});
|
||||
|
||||
if (restoredCount > 0) {
|
||||
toast.success(`${restoredCount}개 파일 컴포넌트 데이터가 DB에서 복원되었습니다. (총 ${fileResponse.totalFiles}개 파일)`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ 파일 컴포넌트 데이터 복원 실패:", error);
|
||||
toast.error("파일 데이터 복원 중 오류가 발생했습니다.");
|
||||
}
|
||||
}, [selectedScreen?.screenId]);
|
||||
|
||||
// 드래그 선택 상태
|
||||
const [selectionDrag, setSelectionDrag] = useState({
|
||||
isSelecting: false,
|
||||
|
|
@ -722,6 +802,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
// 화면 레이아웃 로드
|
||||
useEffect(() => {
|
||||
if (selectedScreen?.screenId) {
|
||||
// 현재 화면 ID를 전역 변수로 설정 (파일 업로드 시 사용)
|
||||
if (typeof window !== 'undefined') {
|
||||
(window as any).__CURRENT_SCREEN_ID__ = selectedScreen.screenId;
|
||||
}
|
||||
|
||||
const loadLayout = async () => {
|
||||
try {
|
||||
const response = await screenApi.getLayout(selectedScreen.screenId);
|
||||
|
|
@ -756,6 +841,9 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
setLayout(layoutWithDefaultGrid);
|
||||
setHistory([layoutWithDefaultGrid]);
|
||||
setHistoryIndex(0);
|
||||
|
||||
// 파일 컴포넌트 데이터 복원 (비동기)
|
||||
restoreFileComponentsData(layoutWithDefaultGrid.components);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("레이아웃 로드 실패:", error);
|
||||
|
|
|
|||
|
|
@ -238,18 +238,76 @@ export const FileComponentConfigPanel: React.FC<FileComponentConfigPanelProps> =
|
|||
|
||||
if (validFiles.length === 0) return;
|
||||
|
||||
// 중복 파일 체크
|
||||
const existingFiles = uploadedFiles;
|
||||
const existingFileNames = existingFiles.map(f => f.realFileName.toLowerCase());
|
||||
const duplicates: string[] = [];
|
||||
const uniqueFiles: File[] = [];
|
||||
|
||||
console.log("🔍 중복 파일 체크:", {
|
||||
uploadedFiles: existingFiles.length,
|
||||
existingFileNames: existingFileNames,
|
||||
newFiles: validFiles.map(f => f.name.toLowerCase())
|
||||
});
|
||||
|
||||
validFiles.forEach(file => {
|
||||
const fileName = file.name.toLowerCase();
|
||||
if (existingFileNames.includes(fileName)) {
|
||||
duplicates.push(file.name);
|
||||
console.log("❌ 중복 파일 발견:", file.name);
|
||||
} else {
|
||||
uniqueFiles.push(file);
|
||||
console.log("✅ 새로운 파일:", file.name);
|
||||
}
|
||||
});
|
||||
|
||||
console.log("🔍 중복 체크 결과:", {
|
||||
duplicates: duplicates,
|
||||
uniqueFiles: uniqueFiles.map(f => f.name)
|
||||
});
|
||||
|
||||
if (duplicates.length > 0) {
|
||||
toast.error(`중복된 파일이 있습니다: ${duplicates.join(', ')}`, {
|
||||
description: "같은 이름의 파일이 이미 업로드되어 있습니다.",
|
||||
duration: 4000
|
||||
});
|
||||
|
||||
if (uniqueFiles.length === 0) {
|
||||
return; // 모든 파일이 중복이면 업로드 중단
|
||||
}
|
||||
|
||||
// 일부만 중복인 경우 고유한 파일만 업로드
|
||||
toast.info(`${uniqueFiles.length}개의 새로운 파일만 업로드합니다.`);
|
||||
}
|
||||
|
||||
const filesToUpload = uniqueFiles.length > 0 ? uniqueFiles : validFiles;
|
||||
|
||||
try {
|
||||
console.log("🔄 파일 업로드 시작:", { fileCount: validFiles.length, uploading });
|
||||
console.log("🔄 파일 업로드 시작:", {
|
||||
originalFiles: validFiles.length,
|
||||
filesToUpload: filesToUpload.length,
|
||||
uploading
|
||||
});
|
||||
setUploading(true);
|
||||
toast.loading(`${validFiles.length}개 파일 업로드 중...`);
|
||||
toast.loading(`${filesToUpload.length}개 파일 업로드 중...`);
|
||||
|
||||
// 그리드와 연동되는 targetObjid 생성 (화면 복원 시스템과 통일)
|
||||
const tableName = 'screen_files';
|
||||
const screenId = (window as any).__CURRENT_SCREEN_ID__ || 'unknown'; // 현재 화면 ID
|
||||
const componentId = component.id;
|
||||
const fieldName = component.columnName || component.id || 'file_attachment';
|
||||
const targetObjid = `${tableName}:${screenId}:${componentId}:${fieldName}`;
|
||||
|
||||
const response = await uploadFiles({
|
||||
files: validFiles,
|
||||
tableName: currentTableName || 'screen_files',
|
||||
fieldName: component.columnName || component.id || 'file_attachment',
|
||||
recordId: component.id,
|
||||
files: filesToUpload,
|
||||
tableName: tableName,
|
||||
fieldName: fieldName,
|
||||
recordId: `${screenId}:${componentId}`, // 화면ID:컴포넌트ID 형태
|
||||
docType: localInputs.docType,
|
||||
docTypeName: localInputs.docTypeName,
|
||||
targetObjid: targetObjid, // 그리드 연동을 위한 targetObjid
|
||||
columnName: fieldName,
|
||||
isVirtualFileColumn: true, // 가상 파일 컬럼으로 처리
|
||||
});
|
||||
|
||||
console.log("📤 파일 업로드 응답:", response);
|
||||
|
|
@ -309,6 +367,27 @@ export const FileComponentConfigPanel: React.FC<FileComponentConfigPanelProps> =
|
|||
localStorageBackup: localStorage.getItem(`fileComponent_${component.id}_files`) ? 'saved' : 'not saved'
|
||||
});
|
||||
|
||||
// 그리드 파일 상태 새로고침 이벤트 발생
|
||||
if (typeof window !== 'undefined') {
|
||||
const refreshEvent = new CustomEvent('refreshFileStatus', {
|
||||
detail: {
|
||||
tableName: tableName,
|
||||
recordId: recordId,
|
||||
columnName: columnName,
|
||||
targetObjid: targetObjid,
|
||||
fileCount: updatedFiles.length
|
||||
}
|
||||
});
|
||||
window.dispatchEvent(refreshEvent);
|
||||
console.log("🔄 FileComponentConfigPanel 그리드 새로고침 이벤트 발생:", {
|
||||
tableName,
|
||||
recordId,
|
||||
columnName,
|
||||
targetObjid,
|
||||
fileCount: updatedFiles.length
|
||||
});
|
||||
}
|
||||
|
||||
toast.dismiss();
|
||||
toast.success(`${validFiles.length}개 파일이 성공적으로 업로드되었습니다.`);
|
||||
console.log("✅ 파일 업로드 성공:", {
|
||||
|
|
@ -375,6 +454,32 @@ export const FileComponentConfigPanel: React.FC<FileComponentConfigPanelProps> =
|
|||
remainingFiles: updatedFiles.length,
|
||||
timestamp: timestamp
|
||||
});
|
||||
|
||||
// 그리드 파일 상태 새로고침 이벤트 발생
|
||||
if (typeof window !== 'undefined') {
|
||||
const tableName = currentTableName || 'screen_files';
|
||||
const recordId = component.id;
|
||||
const columnName = component.columnName || component.id || 'file_attachment';
|
||||
const targetObjid = `${tableName}:${recordId}:${columnName}`;
|
||||
|
||||
const refreshEvent = new CustomEvent('refreshFileStatus', {
|
||||
detail: {
|
||||
tableName: tableName,
|
||||
recordId: recordId,
|
||||
columnName: columnName,
|
||||
targetObjid: targetObjid,
|
||||
fileCount: updatedFiles.length
|
||||
}
|
||||
});
|
||||
window.dispatchEvent(refreshEvent);
|
||||
console.log("🔄 FileComponentConfigPanel 파일 삭제 후 그리드 새로고침:", {
|
||||
tableName,
|
||||
recordId,
|
||||
columnName,
|
||||
targetObjid,
|
||||
fileCount: updatedFiles.length
|
||||
});
|
||||
}
|
||||
|
||||
toast.success('파일이 삭제되었습니다.');
|
||||
} catch (error) {
|
||||
|
|
@ -541,6 +646,41 @@ export const FileComponentConfigPanel: React.FC<FileComponentConfigPanelProps> =
|
|||
}
|
||||
}, [component.id]); // 컴포넌트 ID가 변경될 때만 초기화
|
||||
|
||||
// 전역 파일 상태 변경 감지 (화면 복원 포함)
|
||||
useEffect(() => {
|
||||
const handleGlobalFileStateChange = (event: CustomEvent) => {
|
||||
const { componentId, files, fileCount, isRestore } = event.detail;
|
||||
|
||||
if (componentId === component.id) {
|
||||
console.log("🌐 FileComponentConfigPanel 전역 상태 변경 감지:", {
|
||||
componentId,
|
||||
fileCount,
|
||||
isRestore: !!isRestore,
|
||||
files: files?.map((f: any) => ({ objid: f.objid, name: f.realFileName }))
|
||||
});
|
||||
|
||||
if (files && Array.isArray(files)) {
|
||||
setUploadedFiles(files);
|
||||
|
||||
if (isRestore) {
|
||||
console.log("✅ 파일 컴포넌트 설정 패널 데이터 복원 완료:", {
|
||||
componentId,
|
||||
restoredFileCount: files.length
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('globalFileStateChanged', handleGlobalFileStateChange as EventListener);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('globalFileStateChanged', handleGlobalFileStateChange as EventListener);
|
||||
};
|
||||
}
|
||||
}, [component.id]);
|
||||
|
||||
// 미리 정의된 문서 타입들
|
||||
const docTypeOptions = [
|
||||
{ value: "CONTRACT", label: "계약서" },
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
import { apiClient } from './client';
|
||||
|
||||
export interface ScreenFileInfo {
|
||||
objid: string;
|
||||
savedFileName: string;
|
||||
realFileName: string;
|
||||
fileSize: number;
|
||||
fileExt: string;
|
||||
filePath: string;
|
||||
docType: string;
|
||||
docTypeName: string;
|
||||
targetObjid: string;
|
||||
parentTargetObjid?: string;
|
||||
writer: string;
|
||||
regdate: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface ScreenComponentFilesResponse {
|
||||
success: boolean;
|
||||
componentFiles: { [componentId: string]: ScreenFileInfo[] };
|
||||
totalFiles: number;
|
||||
componentCount: number;
|
||||
}
|
||||
|
||||
export interface ComponentFilesResponse {
|
||||
success: boolean;
|
||||
files: ScreenFileInfo[];
|
||||
componentId: string;
|
||||
screenId: string;
|
||||
}
|
||||
|
||||
export const ScreenFileAPI = {
|
||||
/**
|
||||
* 화면의 모든 컴포넌트별 파일 정보 조회
|
||||
*/
|
||||
async getScreenComponentFiles(screenId: number): Promise<ScreenComponentFilesResponse> {
|
||||
const response = await apiClient.get(`/screen-files/screens/${screenId}/components/files`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 특정 컴포넌트의 파일 목록 조회
|
||||
*/
|
||||
async getComponentFiles(screenId: number, componentId: string): Promise<ComponentFilesResponse> {
|
||||
const response = await apiClient.get(`/screen-files/screens/${screenId}/components/${componentId}/files`);
|
||||
return response.data;
|
||||
}
|
||||
};
|
||||
|
|
@ -54,7 +54,7 @@ export const DynamicWebTypeRenderer: React.FC<DynamicComponentProps> = ({
|
|||
console.log(`DB 웹타입 정보:`, dbWebType);
|
||||
|
||||
// FileWidget의 경우 FileUploadComponent 직접 사용
|
||||
if (dbWebType.component_name === "FileWidget" && webType === "file") {
|
||||
if (dbWebType.component_name === "FileWidget" || webType === "file") {
|
||||
const { FileUploadComponent } = require("@/lib/registry/components/file-upload/FileUploadComponent");
|
||||
console.log(`✅ FileWidget → FileUploadComponent 사용`);
|
||||
return <FileUploadComponent {...props} {...finalProps} />;
|
||||
|
|
@ -75,6 +75,13 @@ export const DynamicWebTypeRenderer: React.FC<DynamicComponentProps> = ({
|
|||
if (webTypeDefinition) {
|
||||
console.log(`웹타입 "${webType}" → 레지스트리 컴포넌트 사용`);
|
||||
|
||||
// 파일 웹타입의 경우 FileUploadComponent 직접 사용
|
||||
if (webType === "file") {
|
||||
const { FileUploadComponent } = require("@/lib/registry/components/file-upload/FileUploadComponent");
|
||||
console.log(`✅ 파일 웹타입 → FileUploadComponent 사용`);
|
||||
return <FileUploadComponent {...props} {...finalProps} />;
|
||||
}
|
||||
|
||||
// 웹타입이 비활성화된 경우
|
||||
if (!webTypeDefinition.isActive) {
|
||||
console.warn(`웹타입 "${webType}"이 비활성화되어 있습니다.`);
|
||||
|
|
@ -99,6 +106,14 @@ export const DynamicWebTypeRenderer: React.FC<DynamicComponentProps> = ({
|
|||
// 3순위: 웹타입명으로 자동 매핑 (폴백)
|
||||
try {
|
||||
console.warn(`웹타입 "${webType}" → 자동 매핑 폴백 사용`);
|
||||
|
||||
// 파일 웹타입의 경우 FileUploadComponent 직접 사용 (최종 폴백)
|
||||
if (webType === "file") {
|
||||
const { FileUploadComponent } = require("@/lib/registry/components/file-upload/FileUploadComponent");
|
||||
console.log(`✅ 폴백: 파일 웹타입 → FileUploadComponent 사용`);
|
||||
return <FileUploadComponent {...props} {...finalProps} />;
|
||||
}
|
||||
|
||||
// const FallbackComponent = getWidgetComponentByWebType(webType);
|
||||
// return <FallbackComponent {...props} />;
|
||||
console.warn(`웹타입 "${webType}" 폴백 기능 임시 비활성화`);
|
||||
|
|
|
|||
|
|
@ -102,10 +102,19 @@ export const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
|||
const componentFiles = (component as any)?.uploadedFiles || [];
|
||||
const lastUpdate = (component as any)?.lastFileUpdate;
|
||||
|
||||
// 전역 상태에서 최신 파일 정보 가져오기
|
||||
const globalFileState = typeof window !== 'undefined' ? (window as any).globalFileState || {} : {};
|
||||
const globalFiles = globalFileState[component.id] || [];
|
||||
|
||||
// 최신 파일 정보 사용 (전역 상태 > 컴포넌트 속성)
|
||||
const currentFiles = globalFiles.length > 0 ? globalFiles : componentFiles;
|
||||
|
||||
console.log("🔄 FileUploadComponent 파일 동기화:", {
|
||||
componentId: component.id,
|
||||
componentFiles: componentFiles.length,
|
||||
currentFiles: uploadedFiles.length,
|
||||
globalFiles: globalFiles.length,
|
||||
currentFiles: currentFiles.length,
|
||||
uploadedFiles: uploadedFiles.length,
|
||||
lastUpdate: lastUpdate
|
||||
});
|
||||
|
||||
|
|
@ -113,7 +122,7 @@ export const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
|||
try {
|
||||
const backupKey = `fileUpload_${component.id}`;
|
||||
const backupFiles = localStorage.getItem(backupKey);
|
||||
if (backupFiles && componentFiles.length === 0) {
|
||||
if (backupFiles && currentFiles.length === 0) {
|
||||
const parsedFiles = JSON.parse(backupFiles);
|
||||
setUploadedFiles(parsedFiles);
|
||||
return;
|
||||
|
|
@ -122,19 +131,66 @@ export const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
|||
console.warn("localStorage 백업 복원 실패:", e);
|
||||
}
|
||||
|
||||
// 컴포넌트 파일과 현재 파일 비교
|
||||
if (JSON.stringify(componentFiles) !== JSON.stringify(uploadedFiles)) {
|
||||
// 최신 파일과 현재 파일 비교
|
||||
if (JSON.stringify(currentFiles) !== JSON.stringify(uploadedFiles)) {
|
||||
console.log("🔄 useEffect에서 파일 목록 변경 감지:", {
|
||||
componentFiles: componentFiles.length,
|
||||
currentFiles: currentFiles.length,
|
||||
uploadedFiles: uploadedFiles.length,
|
||||
componentFilesData: componentFiles.map((f: any) => ({ objid: f.objid, name: f.realFileName })),
|
||||
currentFilesData: currentFiles.map((f: any) => ({ objid: f.objid, name: f.realFileName })),
|
||||
uploadedFilesData: uploadedFiles.map(f => ({ objid: f.objid, name: f.realFileName }))
|
||||
});
|
||||
setUploadedFiles(componentFiles);
|
||||
setUploadedFiles(currentFiles);
|
||||
setForceUpdate(prev => prev + 1);
|
||||
}
|
||||
}, [component.id, (component as any)?.uploadedFiles, (component as any)?.lastFileUpdate]);
|
||||
|
||||
// 전역 상태 변경 감지 (모든 파일 컴포넌트 동기화 + 화면 복원)
|
||||
useEffect(() => {
|
||||
const handleGlobalFileStateChange = (event: CustomEvent) => {
|
||||
const { componentId, files, fileCount, timestamp, isRestore } = event.detail;
|
||||
|
||||
console.log("🔄 FileUploadComponent 전역 상태 변경 감지:", {
|
||||
currentComponentId: component.id,
|
||||
eventComponentId: componentId,
|
||||
isForThisComponent: componentId === component.id,
|
||||
newFileCount: fileCount,
|
||||
currentFileCount: uploadedFiles.length,
|
||||
timestamp,
|
||||
isRestore: !!isRestore
|
||||
});
|
||||
|
||||
// 같은 컴포넌트 ID인 경우에만 업데이트
|
||||
if (componentId === component.id) {
|
||||
const logMessage = isRestore ? "🔄 화면 복원으로 파일 상태 동기화" : "✅ 파일 상태 동기화 적용";
|
||||
console.log(logMessage, {
|
||||
componentId: component.id,
|
||||
이전파일수: uploadedFiles.length,
|
||||
새파일수: files.length,
|
||||
files: files.map((f: any) => ({ objid: f.objid, name: f.realFileName }))
|
||||
});
|
||||
|
||||
setUploadedFiles(files);
|
||||
setForceUpdate(prev => prev + 1);
|
||||
|
||||
// localStorage 백업도 업데이트
|
||||
try {
|
||||
const backupKey = `fileUpload_${component.id}`;
|
||||
localStorage.setItem(backupKey, JSON.stringify(files));
|
||||
} catch (e) {
|
||||
console.warn("localStorage 백업 실패:", e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('globalFileStateChanged', handleGlobalFileStateChange as EventListener);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('globalFileStateChanged', handleGlobalFileStateChange as EventListener);
|
||||
};
|
||||
}
|
||||
}, [component.id, uploadedFiles.length]);
|
||||
|
||||
// 파일 업로드 설정 - componentConfig가 undefined일 수 있으므로 안전하게 처리
|
||||
const safeComponentConfig = componentConfig || {};
|
||||
const fileConfig = {
|
||||
|
|
@ -163,24 +219,80 @@ export const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
|||
const handleFileUpload = useCallback(async (files: File[]) => {
|
||||
if (!files.length) return;
|
||||
|
||||
// 중복 파일 체크
|
||||
const existingFileNames = uploadedFiles.map(f => f.realFileName.toLowerCase());
|
||||
const duplicates: string[] = [];
|
||||
const uniqueFiles: File[] = [];
|
||||
|
||||
console.log("🔍 중복 파일 체크:", {
|
||||
uploadedFiles: uploadedFiles.length,
|
||||
existingFileNames: existingFileNames,
|
||||
newFiles: files.map(f => f.name.toLowerCase())
|
||||
});
|
||||
|
||||
files.forEach(file => {
|
||||
const fileName = file.name.toLowerCase();
|
||||
if (existingFileNames.includes(fileName)) {
|
||||
duplicates.push(file.name);
|
||||
console.log("❌ 중복 파일 발견:", file.name);
|
||||
} else {
|
||||
uniqueFiles.push(file);
|
||||
console.log("✅ 새로운 파일:", file.name);
|
||||
}
|
||||
});
|
||||
|
||||
console.log("🔍 중복 체크 결과:", {
|
||||
duplicates: duplicates,
|
||||
uniqueFiles: uniqueFiles.map(f => f.name)
|
||||
});
|
||||
|
||||
if (duplicates.length > 0) {
|
||||
toast.error(`중복된 파일이 있습니다: ${duplicates.join(', ')}`, {
|
||||
description: "같은 이름의 파일이 이미 업로드되어 있습니다.",
|
||||
duration: 4000
|
||||
});
|
||||
|
||||
if (uniqueFiles.length === 0) {
|
||||
return; // 모든 파일이 중복이면 업로드 중단
|
||||
}
|
||||
|
||||
// 일부만 중복인 경우 고유한 파일만 업로드
|
||||
toast.info(`${uniqueFiles.length}개의 새로운 파일만 업로드합니다.`);
|
||||
}
|
||||
|
||||
const filesToUpload = uniqueFiles.length > 0 ? uniqueFiles : files;
|
||||
setUploadStatus('uploading');
|
||||
toast.loading("파일을 업로드하는 중...", { id: 'file-upload' });
|
||||
|
||||
try {
|
||||
// targetObjid 생성 (InteractiveDataTable과 호환)
|
||||
const tableName = formData?.tableName || component.tableName || 'default_table';
|
||||
const recordId = formData?.id || 'temp_record';
|
||||
const columnName = component.columnName || component.id;
|
||||
const targetObjid = `${tableName}:${recordId}:${columnName}`;
|
||||
|
||||
const uploadData = {
|
||||
tableName: component.tableName || 'default_table',
|
||||
fieldName: component.columnName || component.id,
|
||||
recordId: formData?.id || 'temp_record',
|
||||
tableName: tableName,
|
||||
fieldName: columnName,
|
||||
recordId: recordId,
|
||||
docType: component.fileConfig?.docType || 'DOCUMENT',
|
||||
docTypeName: component.fileConfig?.docTypeName || '일반 문서',
|
||||
targetObjid: targetObjid, // InteractiveDataTable 호환을 위한 targetObjid 추가
|
||||
columnName: columnName, // 가상 파일 컬럼 지원
|
||||
isVirtualFileColumn: true, // 가상 파일 컬럼으로 처리
|
||||
};
|
||||
|
||||
console.log("📤 파일 업로드 시작:", {
|
||||
files: files.map(f => ({ name: f.name, size: f.size })),
|
||||
originalFiles: files.length,
|
||||
filesToUpload: filesToUpload.length,
|
||||
files: filesToUpload.map(f => ({ name: f.name, size: f.size })),
|
||||
uploadData
|
||||
});
|
||||
|
||||
const response = await uploadFiles(files, uploadData);
|
||||
const response = await uploadFiles({
|
||||
files: filesToUpload,
|
||||
...uploadData
|
||||
});
|
||||
|
||||
console.log("📤 파일 업로드 API 응답:", response);
|
||||
|
||||
|
|
@ -239,6 +351,34 @@ export const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
|||
console.warn("localStorage 백업 실패:", e);
|
||||
}
|
||||
|
||||
// 전역 상태 업데이트 (모든 파일 컴포넌트 동기화)
|
||||
if (typeof window !== 'undefined') {
|
||||
// 전역 파일 상태 업데이트
|
||||
const globalFileState = (window as any).globalFileState || {};
|
||||
globalFileState[component.id] = updatedFiles;
|
||||
(window as any).globalFileState = globalFileState;
|
||||
|
||||
// 모든 파일 컴포넌트에 동기화 이벤트 발생
|
||||
const syncEvent = new CustomEvent('globalFileStateChanged', {
|
||||
detail: {
|
||||
componentId: component.id,
|
||||
files: updatedFiles,
|
||||
fileCount: updatedFiles.length,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
});
|
||||
window.dispatchEvent(syncEvent);
|
||||
|
||||
console.log("🌐 전역 파일 상태 업데이트 및 동기화 이벤트 발생:", {
|
||||
componentId: component.id,
|
||||
fileCount: updatedFiles.length,
|
||||
globalState: Object.keys(globalFileState).map(id => ({
|
||||
id,
|
||||
fileCount: globalFileState[id]?.length || 0
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
// 컴포넌트 업데이트
|
||||
if (onUpdate) {
|
||||
const timestamp = Date.now();
|
||||
|
|
@ -255,6 +395,27 @@ export const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
|||
console.warn("⚠️ onUpdate 콜백이 없습니다!");
|
||||
}
|
||||
|
||||
// 그리드 파일 상태 새로고침 이벤트 발생
|
||||
if (typeof window !== 'undefined') {
|
||||
const refreshEvent = new CustomEvent('refreshFileStatus', {
|
||||
detail: {
|
||||
tableName: tableName,
|
||||
recordId: recordId,
|
||||
columnName: columnName,
|
||||
targetObjid: targetObjid,
|
||||
fileCount: updatedFiles.length
|
||||
}
|
||||
});
|
||||
window.dispatchEvent(refreshEvent);
|
||||
console.log("🔄 그리드 파일 상태 새로고침 이벤트 발생:", {
|
||||
tableName,
|
||||
recordId,
|
||||
columnName,
|
||||
targetObjid,
|
||||
fileCount: updatedFiles.length
|
||||
});
|
||||
}
|
||||
|
||||
// 폼 데이터 업데이트
|
||||
if (onFormDataChange && component.columnName) {
|
||||
const fileIds = updatedFiles.map(f => f.objid);
|
||||
|
|
@ -310,6 +471,31 @@ export const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
|||
console.warn("localStorage 백업 업데이트 실패:", e);
|
||||
}
|
||||
|
||||
// 전역 상태 업데이트 (모든 파일 컴포넌트 동기화)
|
||||
if (typeof window !== 'undefined') {
|
||||
// 전역 파일 상태 업데이트
|
||||
const globalFileState = (window as any).globalFileState || {};
|
||||
globalFileState[component.id] = updatedFiles;
|
||||
(window as any).globalFileState = globalFileState;
|
||||
|
||||
// 모든 파일 컴포넌트에 동기화 이벤트 발생
|
||||
const syncEvent = new CustomEvent('globalFileStateChanged', {
|
||||
detail: {
|
||||
componentId: component.id,
|
||||
files: updatedFiles,
|
||||
fileCount: updatedFiles.length,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
});
|
||||
window.dispatchEvent(syncEvent);
|
||||
|
||||
console.log("🗑️ 파일 삭제 후 전역 상태 동기화:", {
|
||||
componentId: component.id,
|
||||
deletedFile: fileName,
|
||||
remainingFiles: updatedFiles.length
|
||||
});
|
||||
}
|
||||
|
||||
// 컴포넌트 업데이트
|
||||
if (onUpdate) {
|
||||
const timestamp = Date.now();
|
||||
|
|
@ -407,16 +593,16 @@ export const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
|||
${safeComponentConfig.disabled ? 'opacity-50 cursor-not-allowed' : 'hover:border-gray-400'}
|
||||
${uploadStatus === 'uploading' ? 'opacity-75' : ''}
|
||||
`}
|
||||
onClick={handleClick}
|
||||
onClick={handleClick}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
onDragStart={onDragStart}
|
||||
onDragEnd={onDragEnd}
|
||||
>
|
||||
<input
|
||||
onDragStart={onDragStart}
|
||||
onDragEnd={onDragEnd}
|
||||
>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
type="file"
|
||||
multiple={safeComponentConfig.multiple}
|
||||
accept={safeComponentConfig.accept}
|
||||
onChange={handleInputChange}
|
||||
|
|
@ -465,75 +651,24 @@ export const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
|||
</div>
|
||||
|
||||
{uploadedFiles.length > 0 ? (
|
||||
uploadedFiles.map((file) => (
|
||||
<Card key={file.objid} className="p-3">
|
||||
<CardContent className="p-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3 flex-1 min-w-0">
|
||||
<div className="flex-shrink-0">
|
||||
{getFileIcon(file.fileExt)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-gray-900 truncate">
|
||||
{file.realFileName}
|
||||
</p>
|
||||
<div className="flex items-center space-x-2 text-xs text-gray-500">
|
||||
<span>{formatFileSize(file.fileSize)}</span>
|
||||
<span>•</span>
|
||||
<span>{file.fileExt.toUpperCase()}</span>
|
||||
{file.uploadedAt && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<span>{new Date(file.uploadedAt).toLocaleDateString()}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-1 flex-shrink-0">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleFileView(file)}
|
||||
className="h-8 w-8 p-0"
|
||||
title="미리보기"
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleFileDownload(file)}
|
||||
className="h-8 w-8 p-0"
|
||||
title="다운로드"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleFileDelete(file)}
|
||||
className="h-8 w-8 p-0 text-red-500 hover:text-red-700"
|
||||
title="삭제"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{uploadedFiles.map((file) => (
|
||||
<div key={file.objid} className="flex items-center space-x-2 p-2 bg-gray-50 rounded text-sm">
|
||||
<div className="flex-shrink-0">
|
||||
{getFileIcon(file.fileExt)}
|
||||
</div>
|
||||
|
||||
{/* 파일 상태 표시 */}
|
||||
{file.status !== 'ACTIVE' && (
|
||||
<div className="mt-2 flex items-center space-x-2">
|
||||
<AlertCircle className="w-4 h-4 text-yellow-500" />
|
||||
<span className="text-xs text-yellow-600">
|
||||
파일 상태: {file.status}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))
|
||||
<span className="flex-1 truncate text-gray-900">
|
||||
{file.realFileName}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
{formatFileSize(file.fileSize)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="text-xs text-gray-500 mt-2 text-center">
|
||||
💡 파일 관리는 상세설정에서 가능합니다
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-8 text-gray-500">
|
||||
<File className="w-12 h-12 mb-3 text-gray-300" />
|
||||
|
|
@ -541,7 +676,7 @@ export const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
|||
<p className="text-xs text-gray-400 mt-1">상세설정에서 파일을 업로드하세요</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue