diff --git a/backend-node/src/controllers/fileController.ts b/backend-node/src/controllers/fileController.ts
index 2feb6bfd..78193960 100644
--- a/backend-node/src/controllers/fileController.ts
+++ b/backend-node/src/controllers/fileController.ts
@@ -767,8 +767,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"
diff --git a/frontend/components/admin/dashboard/CanvasElement.tsx b/frontend/components/admin/dashboard/CanvasElement.tsx
index 90eec9ca..09ddfe5c 100644
--- a/frontend/components/admin/dashboard/CanvasElement.tsx
+++ b/frontend/components/admin/dashboard/CanvasElement.tsx
@@ -26,80 +26,108 @@ import {
// 위젯 동적 임포트
const WeatherWidget = dynamic(() => import("@/components/dashboard/widgets/WeatherWidget"), {
ssr: false,
- loading: () =>
로딩 중...
,
+ loading: () => (
+ 로딩 중...
+ ),
});
const ExchangeWidget = dynamic(() => import("@/components/dashboard/widgets/ExchangeWidget"), {
ssr: false,
- loading: () => 로딩 중...
,
+ loading: () => (
+ 로딩 중...
+ ),
});
const CalculatorWidget = dynamic(() => import("@/components/dashboard/widgets/CalculatorWidget"), {
ssr: false,
- loading: () => 로딩 중...
,
+ loading: () => (
+ 로딩 중...
+ ),
});
const VehicleStatusWidget = dynamic(() => import("@/components/dashboard/widgets/VehicleStatusWidget"), {
ssr: false,
- loading: () => 로딩 중...
,
+ loading: () => (
+ 로딩 중...
+ ),
});
const VehicleListWidget = dynamic(() => import("@/components/dashboard/widgets/VehicleListWidget"), {
ssr: false,
- loading: () => 로딩 중...
,
+ loading: () => (
+ 로딩 중...
+ ),
});
const VehicleMapOnlyWidget = dynamic(() => import("@/components/dashboard/widgets/VehicleMapOnlyWidget"), {
ssr: false,
- loading: () => 로딩 중...
,
+ loading: () => (
+ 로딩 중...
+ ),
});
// 범용 지도 위젯 (차량, 창고, 고객 등 모든 위치 위젯 통합)
const MapSummaryWidget = dynamic(() => import("@/components/dashboard/widgets/MapSummaryWidget"), {
ssr: false,
- loading: () => 로딩 중...
,
+ loading: () => (
+ 로딩 중...
+ ),
});
// 🧪 테스트용 지도 위젯 (REST API 지원)
const MapTestWidget = dynamic(() => import("@/components/dashboard/widgets/MapTestWidget"), {
ssr: false,
- loading: () => 로딩 중...
,
+ loading: () => (
+ 로딩 중...
+ ),
});
// 🧪 테스트용 지도 위젯 V2 (다중 데이터 소스)
const MapTestWidgetV2 = dynamic(() => import("@/components/dashboard/widgets/MapTestWidgetV2"), {
ssr: false,
- loading: () => 로딩 중...
,
+ loading: () => (
+ 로딩 중...
+ ),
});
// 🧪 테스트용 차트 위젯 (다중 데이터 소스)
const ChartTestWidget = dynamic(() => import("@/components/dashboard/widgets/ChartTestWidget"), {
ssr: false,
- loading: () => 로딩 중...
,
+ loading: () => (
+ 로딩 중...
+ ),
});
const ListTestWidget = dynamic(
() => import("@/components/dashboard/widgets/ListTestWidget").then((mod) => ({ default: mod.ListTestWidget })),
{
ssr: false,
- loading: () => 로딩 중...
,
+ loading: () => (
+ 로딩 중...
+ ),
},
);
const CustomMetricTestWidget = dynamic(() => import("@/components/dashboard/widgets/CustomMetricTestWidget"), {
ssr: false,
- loading: () => 로딩 중...
,
+ loading: () => (
+ 로딩 중...
+ ),
});
const RiskAlertTestWidget = dynamic(() => import("@/components/dashboard/widgets/RiskAlertTestWidget"), {
ssr: false,
- loading: () => 로딩 중...
,
+ loading: () => (
+ 로딩 중...
+ ),
});
// 범용 상태 요약 위젯 (차량, 배송 등 모든 상태 위젯 통합)
const StatusSummaryWidget = dynamic(() => import("@/components/dashboard/widgets/StatusSummaryWidget"), {
ssr: false,
- loading: () => 로딩 중...
,
+ loading: () => (
+ 로딩 중...
+ ),
});
// 범용 목록 위젯 (차량, 기사, 제품 등 모든 목록 위젯 통합) - 다른 분 작업 중, 임시 주석
@@ -128,22 +156,30 @@ const StatusSummaryWidget = dynamic(() => import("@/components/dashboard/widgets
const RiskAlertWidget = dynamic(() => import("@/components/dashboard/widgets/RiskAlertWidget"), {
ssr: false,
- loading: () => 로딩 중...
,
+ loading: () => (
+ 로딩 중...
+ ),
});
const TaskWidget = dynamic(() => import("@/components/dashboard/widgets/TaskWidget"), {
ssr: false,
- loading: () => 로딩 중...
,
+ loading: () => (
+ 로딩 중...
+ ),
});
const BookingAlertWidget = dynamic(() => import("@/components/dashboard/widgets/BookingAlertWidget"), {
ssr: false,
- loading: () => 로딩 중...
,
+ loading: () => (
+ 로딩 중...
+ ),
});
const DocumentWidget = dynamic(() => import("@/components/dashboard/widgets/DocumentWidget"), {
ssr: false,
- loading: () => 로딩 중...
,
+ loading: () => (
+ 로딩 중...
+ ),
});
// 시계 위젯 임포트
@@ -160,25 +196,33 @@ import { Button } from "@/components/ui/button";
// 야드 관리 3D 위젯
const YardManagement3DWidget = dynamic(() => import("./widgets/YardManagement3DWidget"), {
ssr: false,
- loading: () => 로딩 중...
,
+ loading: () => (
+ 로딩 중...
+ ),
});
// 작업 이력 위젯
const WorkHistoryWidget = dynamic(() => import("@/components/dashboard/widgets/WorkHistoryWidget"), {
ssr: false,
- loading: () => 로딩 중...
,
+ loading: () => (
+ 로딩 중...
+ ),
});
// 커스텀 통계 카드 위젯
const CustomStatsWidget = dynamic(() => import("@/components/dashboard/widgets/CustomStatsWidget"), {
ssr: false,
- loading: () => 로딩 중...
,
+ loading: () => (
+ 로딩 중...
+ ),
});
// 사용자 커스텀 카드 위젯
const CustomMetricWidget = dynamic(() => import("@/components/dashboard/widgets/CustomMetricWidget"), {
ssr: false,
- loading: () => 로딩 중...
,
+ loading: () => (
+ 로딩 중...
+ ),
});
interface CanvasElementProps {
@@ -758,7 +802,7 @@ export function CanvasElement({
{element.customTitle || element.title}
+ {element.customTitle || element.title}
) : null}
@@ -817,7 +861,7 @@ export function CanvasElement({
e.stopPropagation()}
title="삭제"
@@ -831,9 +875,9 @@ export function CanvasElement({
{element.type === "chart" ? (
// 차트 렌더링
-
+
{isLoadingData ? (
-
+
데이터 로딩 중...
@@ -921,7 +965,12 @@ export function CanvasElement({
) : element.type === "widget" && element.subtype === "status-summary" ? (
// 커스텀 상태 카드 - 범용 위젯
-
+
) : /* element.type === "widget" && element.subtype === "list-summary" ? (
// 커스텀 목록 카드 - 범용 위젯 (다른 분 작업 중 - 임시 주석)
@@ -1106,7 +1155,7 @@ function ResizeHandle({ position, onMouseDown }: ResizeHandleProps) {
return (
onMouseDown(e, position)}
/>
);
diff --git a/frontend/lib/registry/components/file-upload/FileManagerModal.tsx b/frontend/lib/registry/components/file-upload/FileManagerModal.tsx
index cfed8223..469aa1f0 100644
--- a/frontend/lib/registry/components/file-upload/FileManagerModal.tsx
+++ b/frontend/lib/registry/components/file-upload/FileManagerModal.tsx
@@ -52,6 +52,8 @@ export const FileManagerModal: React.FC
= ({
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);
// 파일 아이콘 가져오기
@@ -141,10 +143,49 @@ export const FileManagerModal: React.FC = ({
setViewerFile(null);
};
+ // 파일 클릭 시 미리보기 로드
+ const handleFileClick = async (file: FileInfo) => {
+ setSelectedFile(file);
+
+ // 이미지 파일인 경우 미리보기 로드
+ const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'];
+ if (imageExtensions.includes(file.fileExt.toLowerCase())) {
+ try {
+ // 이전 Blob URL 해제
+ if (previewImageUrl) {
+ URL.revokeObjectURL(previewImageUrl);
+ }
+
+ const { apiClient } = await import("@/lib/api/client");
+ const response = await apiClient.get(`/files/preview/${file.objid}`, {
+ responseType: 'blob'
+ });
+
+ const blob = new Blob([response.data]);
+ const blobUrl = URL.createObjectURL(blob);
+ setPreviewImageUrl(blobUrl);
+ } catch (error) {
+ console.error("이미지 로드 실패:", error);
+ setPreviewImageUrl(null);
+ }
+ } else {
+ setPreviewImageUrl(null);
+ }
+ };
+
+ // 컴포넌트 언마운트 시 Blob URL 해제
+ React.useEffect(() => {
+ return () => {
+ if (previewImageUrl) {
+ URL.revokeObjectURL(previewImageUrl);
+ }
+ };
+ }, [previewImageUrl]);
+
return (
<>
{}}>
-
+
파일 관리 ({uploadedFiles.length}개)
@@ -160,17 +201,21 @@ export const FileManagerModal: React.FC = ({
-
- {/* 파일 업로드 영역 */}
+
+ {/* 파일 업로드 영역 - 높이 축소 */}
{!isDesignMode && (
{
+ if (!config.disabled && !isDesignMode) {
+ fileInputRef.current?.click();
+ }
+ }}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
@@ -186,47 +231,71 @@ export const FileManagerModal: React.FC
= ({
/>
{uploading ? (
-
-
-
업로드 중...
+
) : (
-
-
-
+
+
+
파일을 드래그하거나 클릭하여 업로드하세요
-
- {config.accept && `지원 형식: ${config.accept}`}
- {config.maxSize && ` • 최대 ${formatFileSize(config.maxSize)}`}
- {config.multiple && ' • 여러 파일 선택 가능'}
-
)}
)}
- {/* 파일 목록 */}
-
-
-
-
- 업로드된 파일
-
- {uploadedFiles.length > 0 && (
-
- 총 {formatFileSize(uploadedFiles.reduce((sum, file) => sum + file.fileSize, 0))}
-
- )}
+ {/* 좌우 분할 레이아웃 */}
+
+ {/* 좌측: 이미지 미리보기 */}
+
+ {selectedFile && previewImageUrl ? (
+
+ ) : selectedFile ? (
+
+ {getFileIcon(selectedFile.fileExt)}
+
미리보기 불가능
+
+ ) : (
+
+
+
파일을 선택하면 미리보기가 표시됩니다
+
+ )}
+
+
+ {/* 우측: 파일 목록 */}
+
+
+
+
+ 업로드된 파일
+
+ {uploadedFiles.length > 0 && (
+
+ 총 {formatFileSize(uploadedFiles.reduce((sum, file) => sum + file.fileSize, 0))}
+
+ )}
+
- {uploadedFiles.length > 0 ? (
-
- {uploadedFiles.map((file) => (
-
+
+ {uploadedFiles.length > 0 ? (
+
+ {uploadedFiles.map((file) => (
+
handleFileClick(file)}
+ >
{getFileIcon(file.fileExt)}
@@ -250,40 +319,52 @@ export const FileManagerModal: React.FC
= ({
onSetRepresentative(file)}
+ className="h-7 w-7 p-0"
+ onClick={(e) => {
+ e.stopPropagation();
+ onSetRepresentative(file);
+ }}
title={file.isRepresentative ? "현재 대표 파일" : "대표 파일로 설정"}
>
-
+
)}
handleFileViewInternal(file)}
+ className="h-7 w-7 p-0"
+ onClick={(e) => {
+ e.stopPropagation();
+ handleFileViewInternal(file);
+ }}
title="미리보기"
>
-
+
onFileDownload(file)}
+ className="h-7 w-7 p-0"
+ onClick={(e) => {
+ e.stopPropagation();
+ onFileDownload(file);
+ }}
title="다운로드"
>
-
+
{!isDesignMode && (
onFileDelete(file)}
+ className="h-7 w-7 p-0 text-red-500 hover:text-red-700"
+ onClick={(e) => {
+ e.stopPropagation();
+ onFileDelete(file);
+ }}
title="삭제"
>
-
+
)}
@@ -291,17 +372,18 @@ export const FileManagerModal: React.FC
= ({
))}
) : (
-
-
-
업로드된 파일이 없습니다
-
- {isDesignMode ? '디자인 모드에서는 파일을 업로드할 수 없습니다' : '위의 영역에 파일을 드래그하거나 클릭하여 업로드하세요'}
+
+
+
업로드된 파일이 없습니다
+
+ {isDesignMode ? '디자인 모드에서는 파일을 업로드할 수 없습니다' : '위의 영역에 파일을 업로드하세요'}
)}
+