ui, 파일업로드 관련 손보기

This commit is contained in:
leeheejin 2025-09-29 17:21:47 +09:00
parent bff7416cd1
commit a5bf6601a0
18 changed files with 858 additions and 308 deletions

View File

@ -117,10 +117,10 @@ export default function ScreenViewPage() {
if (loading) {
return (
<div className="flex h-full min-h-[400px] w-full items-center justify-center bg-white">
<div className="text-center">
<Loader2 className="mx-auto h-8 w-8 animate-spin text-blue-600" />
<p className="mt-2 text-gray-600"> ...</p>
<div className="flex h-full min-h-[400px] w-full items-center justify-center bg-gradient-to-br from-gray-50 to-slate-100">
<div className="text-center bg-white rounded-xl border border-gray-200/60 shadow-lg p-8">
<Loader2 className="mx-auto h-10 w-10 animate-spin text-blue-600" />
<p className="mt-4 text-gray-700 font-medium"> ...</p>
</div>
</div>
);
@ -128,14 +128,14 @@ export default function ScreenViewPage() {
if (error || !screen) {
return (
<div className="flex h-full min-h-[400px] w-full items-center justify-center bg-white">
<div className="text-center">
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-red-100">
<span className="text-2xl"></span>
<div className="flex h-full min-h-[400px] w-full items-center justify-center bg-gradient-to-br from-gray-50 to-slate-100">
<div className="text-center bg-white rounded-xl border border-gray-200/60 shadow-lg p-8 max-w-md">
<div className="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-gradient-to-br from-red-100 to-orange-100 shadow-sm">
<span className="text-3xl"></span>
</div>
<h2 className="mb-2 text-xl font-semibold text-gray-900"> </h2>
<p className="mb-4 text-gray-600">{error || "요청하신 화면이 존재하지 않습니다."}</p>
<Button onClick={() => router.back()} variant="outline">
<h2 className="mb-3 text-xl font-bold text-gray-900"> </h2>
<p className="mb-6 text-gray-600 leading-relaxed">{error || "요청하신 화면이 존재하지 않습니다."}</p>
<Button onClick={() => router.back()} variant="outline" className="rounded-lg">
</Button>
</div>
@ -148,17 +148,17 @@ export default function ScreenViewPage() {
const screenHeight = layout?.screenResolution?.height || 800;
return (
<div className="h-full w-full overflow-auto bg-white pt-10">
<div className="h-full w-full overflow-auto bg-gradient-to-br from-gray-50 to-slate-100 pt-10">
{layout && layout.components.length > 0 ? (
// 캔버스 컴포넌트들을 정확한 해상도로 표시
<div
className="relative bg-white"
className="relative bg-white rounded-xl border border-gray-200/60 shadow-lg shadow-gray-900/5 mx-auto"
style={{
width: `${screenWidth}px`,
height: `${screenHeight}px`,
minWidth: `${screenWidth}px`,
minHeight: `${screenHeight}px`,
margin: "0", // mx-auto 제거하여 사이드바 오프셋 방지
margin: "0 auto 40px auto", // 하단 여백 추가
}}
>
{layout.components
@ -178,15 +178,16 @@ export default function ScreenViewPage() {
width: `${component.size.width}px`,
height: `${component.size.height}px`,
zIndex: component.position.z || 1,
backgroundColor: (component as any).backgroundColor || "rgba(59, 130, 246, 0.1)",
border: (component as any).border || "2px dashed #3b82f6",
borderRadius: (component as any).borderRadius || "8px",
padding: "16px",
backgroundColor: (component as any).backgroundColor || "rgba(59, 130, 246, 0.05)",
border: (component as any).border || "1px solid rgba(59, 130, 246, 0.2)",
borderRadius: (component as any).borderRadius || "12px",
padding: "20px",
boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)",
}}
>
{/* 그룹 제목 */}
{(component as any).title && (
<div className="mb-2 text-sm font-medium text-blue-700">{(component as any).title}</div>
<div className="mb-3 text-sm font-semibold text-blue-700 bg-blue-50 px-3 py-1 rounded-lg inline-block">{(component as any).title}</div>
)}
{/* 그룹 내 자식 컴포넌트들 렌더링 */}

View File

@ -227,7 +227,7 @@ export const FloatingPanel: React.FC<FloatingPanelProps> = ({
<div
ref={panelRef}
className={cn(
"fixed z-[9998] rounded-lg border border-gray-200 bg-white shadow-lg",
"fixed z-[9998] rounded-xl border border-gray-200/60 bg-white/95 backdrop-blur-sm shadow-xl shadow-gray-900/10",
isDragging ? "cursor-move shadow-2xl" : "transition-all duration-200 ease-in-out",
isResizing && "cursor-se-resize",
className,
@ -246,7 +246,7 @@ export const FloatingPanel: React.FC<FloatingPanelProps> = ({
<div
ref={dragHandleRef}
data-header="true"
className="flex cursor-move items-center justify-between rounded-t-lg border-b border-gray-200 bg-gray-50 p-3"
className="flex cursor-move items-center justify-between rounded-t-xl border-b border-gray-200/60 bg-gradient-to-r from-gray-50 to-slate-50 p-4"
onMouseDown={handleDragStart}
style={{
userSelect: "none", // 텍스트 선택 방지
@ -259,8 +259,8 @@ export const FloatingPanel: React.FC<FloatingPanelProps> = ({
<GripVertical className="h-4 w-4 text-gray-400" />
<h3 className="text-sm font-medium text-gray-900">{title}</h3>
</div>
<button onClick={onClose} className="rounded p-1 transition-colors hover:bg-gray-200">
<X className="h-4 w-4 text-gray-500" />
<button onClick={onClose} className="rounded-lg p-2 transition-all duration-200 hover:bg-white/80 hover:shadow-sm">
<X className="h-4 w-4 text-gray-500 hover:text-gray-700" />
</button>
</div>
@ -282,7 +282,7 @@ export const FloatingPanel: React.FC<FloatingPanelProps> = ({
{/* 리사이즈 핸들 */}
{resizable && !autoHeight && (
<div className="absolute right-0 bottom-0 h-4 w-4 cursor-se-resize" onMouseDown={handleResizeStart}>
<div className="absolute right-1 bottom-1 h-2 w-2 rounded-sm bg-gray-400" />
<div className="absolute right-1 bottom-1 h-2 w-2 rounded-sm bg-gradient-to-br from-gray-400 to-gray-500 shadow-sm" />
</div>
)}
</div>

View File

@ -37,6 +37,7 @@ import {
RotateCw,
Folder,
FolderOpen,
Grid,
} from "lucide-react";
import { tableTypeApi } from "@/lib/api/screen";
import { commonCodeApi } from "@/lib/api/commonCode";
@ -1721,7 +1722,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
};
return (
<div className={cn("flex h-full flex-col", className)} style={{ ...style, minHeight: "680px" }}>
<div className={cn("flex h-full flex-col rounded-xl border border-gray-200/60 bg-gradient-to-br from-white to-gray-50/30 shadow-sm", className)} style={{ ...style, minHeight: "680px" }}>
{/* 헤더 */}
<div className="p-6 pb-3">
<div className="flex items-center justify-between">
@ -1811,7 +1812,8 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
<div className="flex h-full flex-col">
{visibleColumns.length > 0 ? (
<>
<Table>
<div className="rounded-lg border border-gray-200/60 bg-white shadow-sm overflow-hidden">
<Table>
<TableHeader>
<TableRow>
{/* 체크박스 컬럼 (삭제 기능이 활성화된 경우) */}
@ -1826,7 +1828,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
{visibleColumns.map((column: DataTableColumn) => (
<TableHead
key={column.id}
className="px-4 font-semibold"
className="px-4 font-semibold text-gray-700 bg-gradient-to-r from-gray-50 to-slate-50"
style={{ width: `${((column.gridColumns || 2) / totalGridColumns) * 100}%` }}
>
{column.label}
@ -1850,7 +1852,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
</TableRow>
) : data.length > 0 ? (
data.map((row, rowIndex) => (
<TableRow key={rowIndex} className="hover:bg-muted/50">
<TableRow key={rowIndex} className="hover:bg-gradient-to-r hover:from-blue-50/50 hover:to-indigo-50/30 transition-all duration-200">
{/* 체크박스 셀 (삭제 기능이 활성화된 경우) */}
{component.enableDelete && (
<TableCell className="w-12 px-4">
@ -1861,7 +1863,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
</TableCell>
)}
{visibleColumns.map((column: DataTableColumn) => (
<TableCell key={column.id} className="px-4 font-mono text-sm">
<TableCell key={column.id} className="px-4 text-sm font-medium text-gray-900">
{formatCellValue(row[column.columnName], column, row)}
</TableCell>
))}
@ -1884,10 +1886,11 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
)}
</TableBody>
</Table>
</div>
{/* 페이지네이션 */}
{component.pagination?.enabled && totalPages > 1 && (
<div className="bg-muted/20 mt-auto border-t">
<div className="bg-gradient-to-r from-gray-50 to-slate-50 mt-auto border-t border-gray-200/60">
<div className="flex items-center justify-between px-6 py-3">
{component.pagination.showPageInfo && (
<div className="text-muted-foreground text-sm">

View File

@ -1726,17 +1726,19 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
return (
<>
<div className="h-full w-full">
<div className="h-full w-full rounded-xl border border-gray-200/60 bg-gradient-to-br from-white to-gray-50/30 p-4 shadow-sm transition-all duration-200 hover:shadow-md">
{/* 라벨이 있는 경우 표시 (데이터 테이블 제외) */}
{shouldShowLabel && (
<div className="block" style={labelStyle}>
{labelText}
{component.required && <span style={{ color: "#f97316", marginLeft: "2px" }}>*</span>}
<div className="block mb-3" style={labelStyle}>
<div className="inline-flex items-center bg-gray-100 px-3 py-1 rounded-lg text-sm font-semibold">
{labelText}
{component.required && <span style={{ color: "#f97316", marginLeft: "4px" }}>*</span>}
</div>
</div>
)}
{/* 실제 위젯 */}
<div className="h-full w-full">{renderInteractiveWidget(component)}</div>
<div className="flex-1 rounded-lg overflow-hidden">{renderInteractiveWidget(component)}</div>
</div>
{/* 개선된 검증 패널 (선택적 표시) */}

View File

@ -440,13 +440,18 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
isInteractive={true}
isDesignMode={false}
formData={{
screenId, // 화면 ID 전달
tableName: screenInfo?.tableName,
screenId, // 🎯 화면 ID 전달
// 🎯 백엔드 API가 기대하는 정확한 형식으로 설정
autoLink: true, // 자동 연결 활성화
linkedTable: 'screen_files', // 연결 테이블
recordId: screenId, // 레코드 ID
columnName: fieldName, // 컬럼명 (중요!)
isVirtualFileColumn: true, // 가상 파일 컬럼
id: formData.id,
...formData
}}
onFormDataChange={(data) => {
console.log("📝 파일 업로드 완료:", data);
console.log("📝 실제 화면 파일 업로드 완료:", data);
if (onFormDataChange) {
Object.entries(data).forEach(([key, value]) => {
onFormDataChange(key, value);
@ -454,11 +459,57 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
}
}}
onUpdate={(updates) => {
console.log("🔄 파일 컴포넌트 업데이트:", updates);
// 파일 업로드 완료 시 formData 업데이터
console.log("🔄🔄🔄 실제 화면 파일 컴포넌트 업데이트:", {
componentId: comp.id,
hasUploadedFiles: !!updates.uploadedFiles,
filesCount: updates.uploadedFiles?.length || 0,
hasLastFileUpdate: !!updates.lastFileUpdate,
updates
});
// 파일 업로드/삭제 완료 시 formData 업데이터
if (updates.uploadedFiles && onFormDataChange) {
onFormDataChange(fieldName, updates.uploadedFiles);
}
// 🎯 화면설계 모드와 동기화를 위한 전역 이벤트 발생 (업로드/삭제 모두)
if (updates.uploadedFiles !== undefined && typeof window !== 'undefined') {
// 업로드인지 삭제인지 판단 (lastFileUpdate가 있으면 변경사항 있음)
const action = updates.lastFileUpdate ? 'update' : 'sync';
const eventDetail = {
componentId: comp.id,
files: updates.uploadedFiles,
fileCount: updates.uploadedFiles.length,
action: action,
timestamp: updates.lastFileUpdate || Date.now(),
source: 'realScreen' // 실제 화면에서 온 이벤트임을 표시
};
console.log("🚀🚀🚀 실제 화면 파일 변경 이벤트 발생:", eventDetail);
const event = new CustomEvent('globalFileStateChanged', {
detail: eventDetail
});
window.dispatchEvent(event);
console.log("✅✅✅ 실제 화면 → 화면설계 모드 동기화 이벤트 발생 완료");
// 추가 지연 이벤트들 (화면설계 모드가 열려있을 때를 대비)
setTimeout(() => {
console.log("🔄 실제 화면 추가 이벤트 발생 (지연 100ms)");
window.dispatchEvent(new CustomEvent('globalFileStateChanged', {
detail: { ...eventDetail, delayed: true }
}));
}, 100);
setTimeout(() => {
console.log("🔄 실제 화면 추가 이벤트 발생 (지연 500ms)");
window.dispatchEvent(new CustomEvent('globalFileStateChanged', {
detail: { ...eventDetail, delayed: true, attempt: 2 }
}));
}, 500);
}
}}
/>
</div>
@ -481,19 +532,19 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
return (
<>
<div className="absolute" style={componentStyle}>
<div className="h-full w-full">
<div className="h-full w-full rounded-xl border border-gray-200/60 bg-gradient-to-br from-white to-gray-50/30 p-4 shadow-sm transition-all duration-200 hover:shadow-md">
{/* 라벨 표시 - 컴포넌트 내부에서 라벨을 처리하므로 외부에서는 표시하지 않음 */}
{!hideLabel && component.label && component.style?.labelDisplay === false && (
<div className="mb-1">
<label className="text-sm font-medium text-gray-700">
<div className="mb-3">
<div className="inline-flex items-center bg-gray-100 px-3 py-1 rounded-lg text-sm font-semibold text-gray-700">
{component.label}
{(component as WidgetComponent).required && <span className="ml-1 text-red-500">*</span>}
</label>
{(component as WidgetComponent).required && <span className="ml-1 text-orange-500">*</span>}
</div>
</div>
)}
{/* 위젯 렌더링 */}
<div className="flex-1">{renderInteractiveWidget(component)}</div>
<div className="flex-1 rounded-lg overflow-hidden">{renderInteractiveWidget(component)}</div>
</div>
</div>

View File

@ -127,31 +127,15 @@ const WidgetRenderer: React.FC<{ component: ComponentData }> = ({ component }) =
className: `w-full h-full ${borderClass}`,
};
// 파일 컴포넌트 강제 체크
// 파일 컴포넌트는 별도 로직에서 처리하므로 여기서는 제외
if (isFileComponent(widget)) {
console.log("🎯 RealtimePreview - 파일 컴포넌트 강제 감지:", {
console.log("🎯 RealtimePreview - 파일 컴포넌트 감지 (별도 처리):", {
componentId: widget.id,
widgetType: widgetType,
isFileComponent: true
});
try {
return (
<DynamicWebTypeRenderer
webType="file"
props={{
...commonProps,
component: widget,
value: undefined, // 미리보기이므로 값은 없음
readonly: readonly,
}}
config={widget.webTypeConfig}
/>
);
} catch (error) {
console.error(`파일 웹타입 렌더링 실패:`, error);
return <div className="text-xs text-gray-500 p-2"> ( )</div>;
}
return <div className="text-xs text-gray-500 p-2"> ( )</div>;
}
// 동적 웹타입 렌더링 사용
@ -242,24 +226,84 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
// 전역 파일 상태 변경 감지 (해당 컴포넌트만)
useEffect(() => {
const handleGlobalFileStateChange = (event: CustomEvent) => {
console.log("🎯🎯🎯 RealtimePreview 이벤트 수신:", {
eventComponentId: event.detail.componentId,
currentComponentId: component.id,
isMatch: event.detail.componentId === component.id,
filesCount: event.detail.files?.length || 0,
action: event.detail.action,
delayed: event.detail.delayed || false,
attempt: event.detail.attempt || 1,
eventDetail: event.detail
});
if (event.detail.componentId === component.id) {
console.log("🔄 RealtimePreview 파일 상태 변경 감지:", {
console.log("✅✅✅ RealtimePreview 파일 상태 변경 감지 - 리렌더링 시작:", {
componentId: component.id,
filesCount: event.detail.files?.length || 0,
action: event.detail.action
action: event.detail.action,
oldTrigger: fileUpdateTrigger,
delayed: event.detail.delayed || false,
attempt: event.detail.attempt || 1
});
setFileUpdateTrigger(prev => {
const newTrigger = prev + 1;
console.log("🔄🔄🔄 fileUpdateTrigger 업데이트:", {
old: prev,
new: newTrigger,
componentId: component.id,
attempt: event.detail.attempt || 1
});
return newTrigger;
});
} else {
console.log("❌ 컴포넌트 ID 불일치:", {
eventComponentId: event.detail.componentId,
currentComponentId: component.id
});
}
};
// 강제 업데이트 함수 등록
const forceUpdate = (componentId: string, files: any[]) => {
console.log("🔥🔥🔥 RealtimePreview 강제 업데이트 호출:", {
targetComponentId: componentId,
currentComponentId: component.id,
isMatch: componentId === component.id,
filesCount: files.length
});
if (componentId === component.id) {
console.log("✅✅✅ RealtimePreview 강제 업데이트 적용:", {
componentId: component.id,
filesCount: files.length,
oldTrigger: fileUpdateTrigger
});
setFileUpdateTrigger(prev => {
const newTrigger = prev + 1;
console.log("🔄🔄🔄 강제 fileUpdateTrigger 업데이트:", {
old: prev,
new: newTrigger,
componentId: component.id
});
return newTrigger;
});
setFileUpdateTrigger(prev => prev + 1);
}
};
if (typeof window !== 'undefined') {
window.addEventListener('globalFileStateChanged', handleGlobalFileStateChange as EventListener);
// 전역 강제 업데이트 함수 등록
if (!(window as any).forceRealtimePreviewUpdate) {
(window as any).forceRealtimePreviewUpdate = forceUpdate;
}
return () => {
window.removeEventListener('globalFileStateChanged', handleGlobalFileStateChange as EventListener);
};
}
}, [component.id]);
}, [component.id, fileUpdateTrigger]);
// 컴포넌트 스타일 계산
const componentStyle = {
@ -350,8 +394,8 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
</div>
)}
{/* 위젯 타입 - 동적 렌더링 */}
{type === "widget" && (
{/* 위젯 타입 - 동적 렌더링 (파일 컴포넌트 제외) */}
{type === "widget" && !isFileComponent(component) && (
<div className="flex h-full flex-col">
<div className="pointer-events-none flex-1">
<WidgetRenderer component={component} />
@ -383,7 +427,7 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
});
return (
<div className="flex h-full flex-col">
<div key={`file-component-${component.id}-${fileUpdateTrigger}`} className="flex h-full flex-col">
<div className="pointer-events-none flex-1 rounded border-2 border-dashed border-gray-300 bg-gray-50 p-2">
{currentFiles.length > 0 ? (
<div className="h-full overflow-y-auto">

View File

@ -754,33 +754,61 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
if (response.success && response.componentFiles) {
console.log("📁 복원할 파일 데이터:", response.componentFiles);
// 각 컴포넌트별로 파일 복원
Object.entries(response.componentFiles).forEach(([componentId, files]) => {
if (Array.isArray(files) && files.length > 0) {
// 전역 상태에 파일 저장
// 각 컴포넌트별로 파일 복원 (전역 상태와 localStorage 우선 적용)
Object.entries(response.componentFiles).forEach(([componentId, serverFiles]) => {
if (Array.isArray(serverFiles) && serverFiles.length > 0) {
// 🎯 전역 상태와 localStorage에서 현재 파일 상태 확인
const globalFileState = typeof window !== 'undefined' ? (window as any).globalFileState || {} : {};
globalFileState[componentId] = files;
const currentGlobalFiles = globalFileState[componentId] || [];
let currentLocalStorageFiles: any[] = [];
if (typeof window !== 'undefined') {
try {
const storedFiles = localStorage.getItem(`fileComponent_${componentId}_files`);
if (storedFiles) {
currentLocalStorageFiles = JSON.parse(storedFiles);
}
} catch (e) {
console.warn("localStorage 파일 파싱 실패:", e);
}
}
// 🎯 우선순위: 전역 상태 > localStorage > 서버 데이터
let finalFiles = serverFiles;
if (currentGlobalFiles.length > 0) {
finalFiles = currentGlobalFiles;
console.log(`📂 컴포넌트 ${componentId} 전역 상태 우선 적용:`, finalFiles.length, "개");
} else if (currentLocalStorageFiles.length > 0) {
finalFiles = currentLocalStorageFiles;
console.log(`📂 컴포넌트 ${componentId} localStorage 우선 적용:`, finalFiles.length, "개");
} else {
console.log(`📂 컴포넌트 ${componentId} 서버 데이터 적용:`, finalFiles.length, "개");
}
// 전역 상태에 파일 저장
globalFileState[componentId] = finalFiles;
if (typeof window !== 'undefined') {
(window as any).globalFileState = globalFileState;
}
// localStorage에도 백업
if (typeof window !== 'undefined') {
localStorage.setItem(`fileComponent_${componentId}_files`, JSON.stringify(files));
localStorage.setItem(`fileComponent_${componentId}_files`, JSON.stringify(finalFiles));
}
console.log(`📂 컴포넌트 ${componentId} 파일 복원:`, files.length, "개");
}
});
// 레이아웃의 컴포넌트들에 파일 정보 적용
// 레이아웃의 컴포넌트들에 파일 정보 적용 (전역 상태 우선)
setLayout(prevLayout => {
const updatedComponents = prevLayout.components.map(comp => {
const componentFiles = response.componentFiles[comp.id];
if (componentFiles && componentFiles.length > 0) {
// 🎯 전역 상태에서 최신 파일 정보 가져오기
const globalFileState = typeof window !== 'undefined' ? (window as any).globalFileState || {} : {};
const finalFiles = globalFileState[comp.id] || [];
if (finalFiles.length > 0) {
return {
...comp,
uploadedFiles: componentFiles,
uploadedFiles: finalFiles,
lastFileUpdate: Date.now()
};
}
@ -3368,7 +3396,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
}
return (
<div className="flex h-screen w-full flex-col bg-gray-100">
<div className="flex h-screen w-full flex-col bg-gradient-to-br from-gray-50 to-slate-100">
{/* 상단 툴바 */}
<DesignerToolbar
screenName={selectedScreen?.screenName}
@ -3388,7 +3416,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
/>
{/* 메인 캔버스 영역 (스크롤 가능한 컨테이너) - 좌우 최소화, 위아래 넉넉한 여유 */}
<div className="relative flex-1 overflow-auto bg-gray-100 px-2 py-6">
<div className="relative flex-1 overflow-auto bg-gradient-to-br from-gray-50 to-slate-100 px-2 py-6">
{/* 해상도 정보 표시 - 적당한 여백 */}
<div className="mb-4 flex items-center justify-center">
<div className="rounded-lg border bg-white px-4 py-2 shadow-sm">

View File

@ -125,126 +125,145 @@ export function ComponentsPanel({ className }: ComponentsPanelProps) {
};
return (
<Card className={className}>
<CardHeader className="pb-3">
<CardTitle className="flex items-center justify-between">
<div className="flex items-center">
<Package className="mr-2 h-5 w-5" />
({componentsByCategory.all.length})
<div className={`h-full bg-gradient-to-br from-slate-50 to-purple-50/30 border-r border-gray-200/60 shadow-sm ${className}`}>
<div className="p-6">
{/* 헤더 */}
<div className="flex items-center justify-between mb-6">
<div>
<h2 className="text-lg font-semibold text-gray-900 mb-1"></h2>
<p className="text-sm text-gray-500">{componentsByCategory.all.length} </p>
</div>
<Button variant="outline" size="sm" onClick={handleRefresh} title="컴포넌트 새로고침">
<Button
variant="outline"
size="sm"
onClick={handleRefresh}
title="컴포넌트 새로고침"
className="bg-white/60 border-gray-200/60 hover:bg-white hover:border-gray-300"
>
<RotateCcw className="h-4 w-4" />
</Button>
</CardTitle>
</div>
{/* 검색창 */}
<div className="relative">
<Search className="text-muted-foreground absolute top-2.5 left-2 h-4 w-4" />
<div className="relative mb-6">
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-gray-400" />
<Input
placeholder="컴포넌트 검색..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-8"
className="pl-10 border-0 bg-white/80 backdrop-blur-sm shadow-sm focus:bg-white transition-colors"
/>
</div>
</CardHeader>
</div>
<CardContent>
<div className="px-6">
<Tabs
value={selectedCategory}
onValueChange={(value) => setSelectedCategory(value as ComponentCategory | "all")}
>
{/* 카테고리 탭 (input 카테고리 제외) */}
<TabsList className="grid w-full grid-cols-3 lg:grid-cols-5">
<TabsTrigger value="all" className="flex items-center">
<TabsList className="grid w-full grid-cols-3 lg:grid-cols-5 bg-white/60 backdrop-blur-sm border-0 p-1">
<TabsTrigger value="all" className="flex items-center text-xs data-[state=active]:bg-purple-600 data-[state=active]:text-white">
<Package className="mr-1 h-3 w-3" />
</TabsTrigger>
<TabsTrigger value="display" className="flex items-center">
<TabsTrigger value="display" className="flex items-center text-xs data-[state=active]:bg-purple-600 data-[state=active]:text-white">
<Palette className="mr-1 h-3 w-3" />
</TabsTrigger>
<TabsTrigger value="action" className="flex items-center">
<TabsTrigger value="action" className="flex items-center text-xs data-[state=active]:bg-purple-600 data-[state=active]:text-white">
<Zap className="mr-1 h-3 w-3" />
</TabsTrigger>
<TabsTrigger value="layout" className="flex items-center">
<TabsTrigger value="layout" className="flex items-center text-xs data-[state=active]:bg-purple-600 data-[state=active]:text-white">
<Layers className="mr-1 h-3 w-3" />
</TabsTrigger>
<TabsTrigger value="utility" className="flex items-center">
<TabsTrigger value="utility" className="flex items-center text-xs data-[state=active]:bg-purple-600 data-[state=active]:text-white">
<Package className="mr-1 h-3 w-3" />
</TabsTrigger>
</TabsList>
{/* 컴포넌트 목록 */}
<div className="mt-4">
<TabsContent value={selectedCategory} className="space-y-2">
<div className="mt-6">
<TabsContent value={selectedCategory} className="space-y-3">
{filteredComponents.length > 0 ? (
<div className="grid max-h-96 grid-cols-1 gap-2 overflow-y-auto">
<div className="grid max-h-96 grid-cols-1 gap-3 overflow-y-auto pr-2">
{filteredComponents.map((component) => (
<div
key={component.id}
draggable
onDragStart={(e) => handleDragStart(e, component)}
className="hover:bg-accent flex cursor-grab items-center rounded-lg border p-3 transition-colors active:cursor-grabbing"
onDragStart={(e) => {
handleDragStart(e, component);
// 드래그 시작 시 시각적 피드백
e.currentTarget.style.opacity = '0.5';
e.currentTarget.style.transform = 'rotate(-3deg) scale(0.95)';
}}
onDragEnd={(e) => {
// 드래그 종료 시 원래 상태로 복원
e.currentTarget.style.opacity = '1';
e.currentTarget.style.transform = 'none';
}}
className="group cursor-grab rounded-lg border border-gray-200/40 bg-white/90 backdrop-blur-sm p-5 shadow-sm transition-all duration-300 hover:bg-white hover:shadow-lg hover:shadow-purple-500/15 hover:scale-[1.02] hover:border-purple-300/60 hover:-translate-y-1 active:cursor-grabbing active:scale-[0.98] active:translate-y-0"
title={component.description}
>
<div className="min-w-0 flex-1">
<div className="mb-1 flex items-center justify-between">
<h4 className="truncate text-sm font-medium">{component.name}</h4>
<div className="flex items-center space-x-1">
{/* 카테고리 뱃지 */}
<Badge variant="secondary" className="text-xs">
{getCategoryIcon(component.category)}
<span className="ml-1">{component.category}</span>
</Badge>
{/* 새 컴포넌트 뱃지 */}
<Badge variant="default" className="bg-green-500 text-xs">
<div className="flex items-start space-x-4">
<div className="flex h-14 w-14 items-center justify-center rounded-xl bg-gradient-to-br from-purple-500 to-purple-600 text-white shadow-md group-hover:shadow-lg group-hover:scale-110 transition-all duration-300">
{getCategoryIcon(component.category)}
</div>
<div className="min-w-0 flex-1">
<div className="flex items-start justify-between mb-2">
<h4 className="font-semibold text-gray-900 text-sm leading-tight">{component.name}</h4>
<Badge variant="default" className="bg-gradient-to-r from-emerald-500 to-emerald-600 text-white text-xs border-0 ml-2 px-2 py-1 rounded-full font-medium shadow-sm">
</Badge>
</div>
</div>
<p className="text-muted-foreground truncate text-xs">{component.description}</p>
<p className="text-xs text-gray-500 line-clamp-2 leading-relaxed mb-3">{component.description}</p>
{/* 웹타입 및 크기 정보 */}
<div className="text-muted-foreground mt-2 flex items-center justify-between text-xs">
<span>: {component.webType}</span>
<span>
{component.defaultSize.width}×{component.defaultSize.height}
</span>
</div>
{/* 태그 */}
{component.tags && component.tags.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{component.tags.slice(0, 3).map((tag, index) => (
<Badge key={index} variant="outline" className="text-xs">
{tag}
</Badge>
))}
{component.tags.length > 3 && (
<Badge variant="outline" className="text-xs">
+{component.tags.length - 3}
</Badge>
)}
<div className="flex items-center justify-between mb-2">
<div className="flex items-center space-x-2 text-xs text-gray-400">
<span className="bg-gradient-to-r from-gray-100 to-gray-200 px-3 py-1 rounded-full font-medium text-gray-600">
{component.defaultSize.width}×{component.defaultSize.height}
</span>
</div>
<span className="text-xs font-medium text-purple-600 capitalize bg-gradient-to-r from-purple-50 to-indigo-50 px-3 py-1 rounded-full border border-purple-200/50">
{component.category}
</span>
</div>
)}
{/* 태그 */}
{component.tags && component.tags.length > 0 && (
<div className="flex flex-wrap gap-1">
{component.tags.slice(0, 2).map((tag, index) => (
<Badge key={index} variant="outline" className="text-xs bg-gradient-to-r from-gray-50 to-gray-100 text-gray-600 border-gray-200/50 rounded-full px-2 py-1">
{tag}
</Badge>
))}
{component.tags.length > 2 && (
<Badge variant="outline" className="text-xs bg-gradient-to-r from-gray-50 to-gray-100 text-gray-600 border-gray-200/50 rounded-full px-2 py-1">
+{component.tags.length - 2}
</Badge>
)}
</div>
)}
</div>
</div>
</div>
))}
</div>
) : (
<div className="text-muted-foreground py-8 text-center">
<Package className="mx-auto mb-3 h-12 w-12 opacity-50" />
<p className="text-sm">
{searchQuery
? `"${searchQuery}"에 대한 검색 결과가 없습니다.`
: "이 카테고리에 컴포넌트가 없습니다."}
</p>
<div className="py-12 text-center text-gray-500">
<div className="p-8">
<Package className="mx-auto mb-3 h-12 w-12 text-gray-300" />
<p className="text-sm font-medium text-gray-600">
{searchQuery
? `"${searchQuery}"에 대한 컴포넌트를 찾을 수 없습니다`
: "이 카테고리에 컴포넌트가 없습니다"}
</p>
<p className="text-xs text-gray-400 mt-1"> </p>
</div>
</div>
)}
</TabsContent>
@ -252,31 +271,40 @@ export function ComponentsPanel({ className }: ComponentsPanelProps) {
</Tabs>
{/* 통계 정보 */}
<div className="mt-4 border-t pt-3">
<div className="mt-6 rounded-xl bg-gradient-to-r from-purple-50 to-pink-50 border border-purple-100/60 p-4">
<div className="grid grid-cols-2 gap-4 text-center">
<div>
<div className="text-lg font-bold text-green-600">{filteredComponents.length}</div>
<div className="text-muted-foreground text-xs"> </div>
<div className="text-lg font-bold text-emerald-600">{filteredComponents.length}</div>
<div className="text-xs text-gray-500"></div>
</div>
<div>
<div className="text-lg font-bold text-blue-600">{allComponents.length}</div>
<div className="text-muted-foreground text-xs"> </div>
<div className="text-lg font-bold text-purple-600">{allComponents.length}</div>
<div className="text-xs text-gray-500"></div>
</div>
</div>
</div>
{/* 개발 정보 (개발 모드에서만) */}
{process.env.NODE_ENV === "development" && (
<div className="mt-4 border-t pt-3">
<div className="text-muted-foreground space-y-1 text-xs">
<div>🔧 </div>
<div> Hot Reload </div>
<div>🛡 </div>
<div className="mt-4 rounded-xl bg-gradient-to-r from-gray-50 to-slate-50 border border-gray-100/60 p-4">
<div className="space-y-1 text-xs text-gray-600">
<div className="flex items-center space-x-2">
<span className="w-2 h-2 bg-blue-500 rounded-full"></span>
<span> </span>
</div>
<div className="flex items-center space-x-2">
<span className="w-2 h-2 bg-green-500 rounded-full"></span>
<span>Hot Reload </span>
</div>
<div className="flex items-center space-x-2">
<span className="w-2 h-2 bg-purple-500 rounded-full"></span>
<span> </span>
</div>
</div>
</div>
)}
</CardContent>
</Card>
</div>
</div>
);
}

View File

@ -28,6 +28,13 @@ export const FileComponentConfigPanel: React.FC<FileComponentConfigPanelProps> =
currentTable,
currentTableName,
}) => {
console.log("🎨🎨🎨 FileComponentConfigPanel 렌더링:", {
componentId: component?.id,
componentType: component?.type,
hasOnUpdateProperty: !!onUpdateProperty,
currentTable,
currentTableName
});
// fileConfig가 없는 경우 초기화
React.useEffect(() => {
if (!component.fileConfig) {
@ -112,13 +119,18 @@ export const FileComponentConfigPanel: React.FC<FileComponentConfigPanelProps> =
const componentFiles = component.uploadedFiles || [];
const globalFiles = getGlobalFileState()[component.id] || [];
// localStorage 백업에서 복원 (영구 저장된 파일 + 임시 파일)
// localStorage 백업에서 복원 (영구 저장된 파일 + 임시 파일 + FileUploadComponent 백업)
const backupKey = `fileComponent_${component.id}_files`;
const tempBackupKey = `fileComponent_${component.id}_files_temp`;
const fileUploadBackupKey = `fileUpload_${component.id}`; // 🎯 실제 화면과 동기화
const backupFiles = localStorage.getItem(backupKey);
const tempBackupFiles = localStorage.getItem(tempBackupKey);
const fileUploadBackupFiles = localStorage.getItem(fileUploadBackupKey); // 🎯 실제 화면 백업
let parsedBackupFiles: FileInfo[] = [];
let parsedTempFiles: FileInfo[] = [];
let parsedFileUploadFiles: FileInfo[] = []; // 🎯 실제 화면 파일
if (backupFiles) {
try {
@ -136,8 +148,18 @@ export const FileComponentConfigPanel: React.FC<FileComponentConfigPanelProps> =
}
}
// 우선순위: 전역 상태 > localStorage > 임시 파일 > 컴포넌트 속성
// 🎯 실제 화면 FileUploadComponent 백업 파싱
if (fileUploadBackupFiles) {
try {
parsedFileUploadFiles = JSON.parse(fileUploadBackupFiles);
} catch (error) {
console.error("FileUploadComponent 백업 파일 파싱 실패:", error);
}
}
// 🎯 우선순위: 전역 상태 > FileUploadComponent 백업 > localStorage > 임시 파일 > 컴포넌트 속성
const finalFiles = globalFiles.length > 0 ? globalFiles :
parsedFileUploadFiles.length > 0 ? parsedFileUploadFiles : // 🎯 실제 화면 우선
parsedBackupFiles.length > 0 ? parsedBackupFiles :
parsedTempFiles.length > 0 ? parsedTempFiles :
componentFiles;
@ -148,8 +170,12 @@ export const FileComponentConfigPanel: React.FC<FileComponentConfigPanelProps> =
globalFiles: globalFiles.length,
backupFiles: parsedBackupFiles.length,
tempFiles: parsedTempFiles.length,
fileUploadFiles: parsedFileUploadFiles.length, // 🎯 실제 화면 파일 수
finalFiles: finalFiles.length,
source: globalFiles.length > 0 ? 'global' : parsedBackupFiles.length > 0 ? 'localStorage' : parsedTempFiles.length > 0 ? 'temp' : 'component'
source: globalFiles.length > 0 ? 'global' :
parsedFileUploadFiles.length > 0 ? 'fileUploadComponent' : // 🎯 실제 화면 소스
parsedBackupFiles.length > 0 ? 'localStorage' :
parsedTempFiles.length > 0 ? 'temp' : 'component'
});
return finalFiles;
@ -190,7 +216,17 @@ export const FileComponentConfigPanel: React.FC<FileComponentConfigPanelProps> =
// 파일 업로드 처리
const handleFileUpload = useCallback(async (files: FileList | File[]) => {
if (!files || files.length === 0) return;
console.log("🚀🚀🚀 FileComponentConfigPanel 파일 업로드 시작:", {
filesCount: files?.length || 0,
componentId: component?.id,
componentType: component?.type,
hasOnUpdateProperty: !!onUpdateProperty
});
if (!files || files.length === 0) {
console.log("❌ 파일이 없음");
return;
}
const fileArray = Array.from(files);
const validFiles: File[] = [];
@ -291,23 +327,49 @@ export const FileComponentConfigPanel: React.FC<FileComponentConfigPanelProps> =
setUploading(true);
toast.loading(`${filesToUpload.length}개 파일 업로드 중...`);
// 그리드와 연동되는 targetObjid 생성 (화면 복원 시스템과 통일)
const tableName = 'screen_files';
const screenId = (window as any).__CURRENT_SCREEN_ID__ || 'unknown'; // 현재 화면 ID
// 🎯 여러 방법으로 screenId 확인
let screenId = (window as any).__CURRENT_SCREEN_ID__;
// 1차: 전역 변수에서 가져오기
if (!screenId) {
// 2차: URL에서 추출 시도
if (typeof window !== 'undefined' && window.location.pathname.includes('/screens/')) {
const pathScreenId = window.location.pathname.split('/screens/')[1];
if (pathScreenId && !isNaN(parseInt(pathScreenId))) {
screenId = parseInt(pathScreenId);
}
}
}
// 3차: 기본값 설정
if (!screenId) {
screenId = 40; // 기본 화면 ID (디자인 모드용)
console.warn("⚠️ screenId를 찾을 수 없어 기본값(40) 사용");
}
const componentId = component.id;
const fieldName = component.columnName || component.id || 'file_attachment';
const targetObjid = `${tableName}:${screenId}:${componentId}:${fieldName}`;
console.log("📋 파일 업로드 기본 정보:", {
screenId,
screenIdSource: (window as any).__CURRENT_SCREEN_ID__ ? 'global' : 'url_or_default',
componentId,
fieldName,
docType: localInputs.docType,
docTypeName: localInputs.docTypeName,
currentPath: typeof window !== 'undefined' ? window.location.pathname : 'unknown'
});
const response = await uploadFiles({
files: filesToUpload,
tableName: tableName,
fieldName: fieldName,
recordId: `screen_${screenId}:${componentId}`, // 템플릿 파일 형태
// 🎯 백엔드 API가 기대하는 정확한 형식으로 설정
autoLink: true, // 자동 연결 활성화
linkedTable: 'screen_files', // 연결 테이블
recordId: screenId, // 레코드 ID
columnName: fieldName, // 컬럼명
isVirtualFileColumn: true, // 가상 파일 컬럼
docType: localInputs.docType,
docTypeName: localInputs.docTypeName,
targetObjid: targetObjid, // 그리드 연동을 위한 targetObjid
columnName: fieldName,
isVirtualFileColumn: true, // 가상 파일 컬럼으로 처리
});
console.log("📤 파일 업로드 응답:", response);
@ -360,15 +422,61 @@ export const FileComponentConfigPanel: React.FC<FileComponentConfigPanelProps> =
// 전역 파일 상태 변경 이벤트 발생 (RealtimePreview 업데이트용)
if (typeof window !== 'undefined') {
const eventDetail = {
componentId: component.id,
files: updatedFiles,
fileCount: updatedFiles.length,
action: 'upload',
timestamp: Date.now(),
source: 'designMode' // 🎯 화면설계 모드에서 온 이벤트임을 표시
};
console.log("🚀🚀🚀 FileComponentConfigPanel 이벤트 발생:", eventDetail);
console.log("🔍 현재 컴포넌트 ID:", component.id);
console.log("🔍 업로드된 파일 수:", updatedFiles.length);
console.log("🔍 파일 목록:", updatedFiles.map(f => f.name));
const event = new CustomEvent('globalFileStateChanged', {
detail: {
componentId: component.id,
files: updatedFiles,
action: 'upload',
timestamp: Date.now()
}
detail: eventDetail
});
// 이벤트 리스너가 있는지 확인
const listenerCount = window.getEventListeners ?
window.getEventListeners(window)?.globalFileStateChanged?.length || 0 :
'unknown';
console.log("🔍 globalFileStateChanged 리스너 수:", listenerCount);
window.dispatchEvent(event);
console.log("✅✅✅ globalFileStateChanged 이벤트 발생 완료");
// 강제로 모든 RealtimePreview 컴포넌트에게 알림 (여러 번)
setTimeout(() => {
console.log("🔄 추가 이벤트 발생 (지연 100ms)");
window.dispatchEvent(new CustomEvent('globalFileStateChanged', {
detail: { ...eventDetail, delayed: true }
}));
}, 100);
setTimeout(() => {
console.log("🔄 추가 이벤트 발생 (지연 300ms)");
window.dispatchEvent(new CustomEvent('globalFileStateChanged', {
detail: { ...eventDetail, delayed: true, attempt: 2 }
}));
}, 300);
setTimeout(() => {
console.log("🔄 추가 이벤트 발생 (지연 500ms)");
window.dispatchEvent(new CustomEvent('globalFileStateChanged', {
detail: { ...eventDetail, delayed: true, attempt: 3 }
}));
}, 500);
// 직접 전역 상태 강제 업데이트
console.log("🔄 전역 상태 강제 업데이트 시도");
if ((window as any).forceRealtimePreviewUpdate) {
(window as any).forceRealtimePreviewUpdate(component.id, updatedFiles);
}
}
console.log("🔄 FileComponentConfigPanel 자동 저장:", {
@ -417,10 +525,18 @@ export const FileComponentConfigPanel: React.FC<FileComponentConfigPanelProps> =
} else {
throw new Error(response.message || '파일 업로드에 실패했습니다.');
}
} catch (error) {
console.error('❌ 파일 업로드 오류:', error);
} catch (error: any) {
console.error('❌ 파일 업로드 오류:', {
error,
errorMessage: error?.message,
errorResponse: error?.response?.data,
errorStatus: error?.response?.status,
componentId: component?.id,
screenId,
fieldName
});
toast.dismiss();
toast.error('파일 업로드에 실패했습니다.');
toast.error(`파일 업로드에 실패했습니다: ${error?.message || '알 수 없는 오류'}`);
} finally {
console.log("🏁 파일 업로드 완료, 로딩 상태 해제");
setUploading(false);
@ -444,8 +560,17 @@ export const FileComponentConfigPanel: React.FC<FileComponentConfigPanelProps> =
// 파일 삭제 처리
const handleFileDelete = useCallback(async (fileId: string) => {
console.log("🗑️🗑️🗑️ FileComponentConfigPanel 파일 삭제 시작:", {
fileId,
componentId: component?.id,
currentFilesCount: uploadedFiles.length,
hasOnUpdateProperty: !!onUpdateProperty
});
try {
console.log("📡 deleteFile API 호출 시작...");
await deleteFile(fileId, 'temp_record');
console.log("✅ deleteFile API 호출 성공");
const updatedFiles = uploadedFiles.filter(file => file.objid !== fileId && file.id !== fileId);
setUploadedFiles(updatedFiles);
@ -473,8 +598,42 @@ export const FileComponentConfigPanel: React.FC<FileComponentConfigPanelProps> =
timestamp: timestamp
});
// 그리드 파일 상태 새로고침 이벤트 발생
if (typeof window !== 'undefined') {
// 🎯 RealtimePreview 동기화를 위한 전역 이벤트 발생
if (typeof window !== 'undefined') {
const eventDetail = {
componentId: component.id,
files: updatedFiles,
fileCount: updatedFiles.length,
action: 'delete',
timestamp: timestamp,
source: 'designMode' // 🎯 화면설계 모드에서 온 이벤트임을 표시
};
console.log("🚀🚀🚀 FileComponentConfigPanel 삭제 이벤트 발생:", eventDetail);
const event = new CustomEvent('globalFileStateChanged', {
detail: eventDetail
});
window.dispatchEvent(event);
console.log("✅✅✅ globalFileStateChanged 삭제 이벤트 발생 완료");
// 추가 지연 이벤트들
setTimeout(() => {
console.log("🔄 추가 삭제 이벤트 발생 (지연 100ms)");
window.dispatchEvent(new CustomEvent('globalFileStateChanged', {
detail: { ...eventDetail, delayed: true }
}));
}, 100);
setTimeout(() => {
console.log("🔄 추가 삭제 이벤트 발생 (지연 300ms)");
window.dispatchEvent(new CustomEvent('globalFileStateChanged', {
detail: { ...eventDetail, delayed: true, attempt: 2 }
}));
}, 300);
// 그리드 파일 상태 새로고침 이벤트도 유지
const tableName = currentTableName || 'screen_files';
const recordId = component.id;
const columnName = component.columnName || component.id || 'file_attachment';
@ -557,12 +716,22 @@ export const FileComponentConfigPanel: React.FC<FileComponentConfigPanelProps> =
e.preventDefault();
setDragOver(false);
const files = e.dataTransfer.files;
console.log("📂 드래그앤드롭 이벤트:", {
filesCount: files.length,
files: files.length > 0 ? Array.from(files).map(f => f.name) : [],
componentId: component?.id
});
if (files.length > 0) {
handleFileUpload(files);
}
}, [handleFileUpload]);
}, [handleFileUpload, component?.id]);
const handleFileSelect = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
console.log("📁 파일 선택 이벤트:", {
filesCount: e.target.files?.length || 0,
files: e.target.files ? Array.from(e.target.files).map(f => f.name) : []
});
const files = e.target.files;
if (files && files.length > 0) {
handleFileUpload(files);
@ -667,20 +836,49 @@ export const FileComponentConfigPanel: React.FC<FileComponentConfigPanelProps> =
// 전역 파일 상태 변경 감지 (화면 복원 포함)
useEffect(() => {
const handleGlobalFileStateChange = (event: CustomEvent) => {
const { componentId, files, fileCount, isRestore } = event.detail;
const { componentId, files, fileCount, isRestore, source } = event.detail;
if (componentId === component.id) {
console.log("🌐 FileComponentConfigPanel 전역 상태 변경 감지:", {
componentId,
fileCount,
isRestore: !!isRestore,
source: source || 'unknown',
files: files?.map((f: any) => ({ objid: f.objid, name: f.realFileName }))
});
if (files && Array.isArray(files)) {
setUploadedFiles(files);
if (isRestore) {
// 🎯 실제 화면에서 온 이벤트이거나 화면 복원인 경우 컴포넌트 속성도 업데이트
if (isRestore || source === 'realScreen') {
console.log("✅✅✅ 실제 화면 → 화면설계 모드 동기화 적용:", {
componentId,
fileCount: files.length,
source: source || 'restore'
});
onUpdateProperty(component.id, "uploadedFiles", files);
onUpdateProperty(component.id, "lastFileUpdate", Date.now());
// localStorage 백업도 업데이트
try {
const backupKey = `fileComponent_${component.id}_files`;
localStorage.setItem(backupKey, JSON.stringify(files));
console.log("💾 실제 화면 동기화 후 localStorage 백업 업데이트:", {
componentId: component.id,
fileCount: files.length
});
} catch (e) {
console.warn("localStorage 백업 업데이트 실패:", e);
}
// 전역 상태 업데이트
setGlobalFileState(prev => ({
...prev,
[component.id]: files
}));
} else if (isRestore) {
console.log("✅ 파일 컴포넌트 설정 패널 데이터 복원 완료:", {
componentId,
restoredFileCount: files.length
@ -697,7 +895,7 @@ export const FileComponentConfigPanel: React.FC<FileComponentConfigPanelProps> =
window.removeEventListener('globalFileStateChanged', handleGlobalFileStateChange as EventListener);
};
}
}, [component.id]);
}, [component.id, onUpdateProperty]);
// 미리 정의된 문서 타입들
const docTypeOptions = [
@ -893,18 +1091,33 @@ export const FileComponentConfigPanel: React.FC<FileComponentConfigPanelProps> =
{/* 파일 업로드 영역 */}
<div className="space-y-2">
<h4 className="text-sm font-medium text-gray-900"> </h4>
<Card>
<CardContent className="p-4">
<Card className="border-gray-200/60 shadow-sm">
<CardContent className="p-6">
<div
className={`
border-2 border-dashed rounded-lg p-4 text-center cursor-pointer transition-colors
${dragOver ? 'border-blue-400 bg-blue-50' : 'border-gray-300'}
${uploading ? 'opacity-50 cursor-not-allowed' : 'hover:border-gray-400'}
border-2 border-dashed rounded-xl p-6 text-center cursor-pointer transition-all duration-300
${dragOver ? 'border-blue-400 bg-gradient-to-br from-blue-50 to-indigo-50 shadow-sm' : 'border-gray-300/60'}
${uploading ? 'opacity-50 cursor-not-allowed' : 'hover:border-blue-400/60 hover:bg-gray-50/50 hover:shadow-sm'}
`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onClick={() => !uploading && document.getElementById('file-input-config')?.click()}
onClick={() => {
console.log("🖱️ 파일 업로드 영역 클릭:", {
uploading,
inputElement: document.getElementById('file-input-config'),
componentId: component?.id
});
if (!uploading) {
const input = document.getElementById('file-input-config');
if (input) {
console.log("✅ 파일 input 클릭 실행");
input.click();
} else {
console.log("❌ 파일 input 요소를 찾을 수 없음");
}
}
}}
>
<input
id="file-input-config"

View File

@ -147,7 +147,7 @@ export default function LayoutsPanel({
};
return (
<div className={`layouts-panel h-full ${className || ""}`}>
<div className={`layouts-panel h-full bg-gradient-to-br from-slate-50 to-indigo-50/30 border-r border-gray-200/60 shadow-sm ${className || ""}`}>
<div className="flex h-full flex-col">
{/* 헤더 */}
<div className="border-b p-4">

View File

@ -487,16 +487,22 @@ export const TemplatesPanel: React.FC<TemplatesPanelProps> = ({ onDragStart }) =
});
return (
<div className="flex h-full flex-col space-y-4 p-4">
<div className="flex h-full flex-col bg-gradient-to-br from-slate-50 to-blue-50/30 p-6 border-r border-gray-200/60 shadow-sm">
{/* 헤더 */}
<div className="mb-6">
<h2 className="text-lg font-semibold text-gray-900 mb-1">릿</h2>
<p className="text-sm text-gray-500"> </p>
</div>
{/* 검색 */}
<div className="space-y-3">
<div className="space-y-4">
<div className="relative">
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-gray-400" />
<Input
placeholder="템플릿 검색..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
className="pl-10 border-0 bg-white/80 backdrop-blur-sm shadow-sm focus:bg-white transition-colors"
/>
</div>
@ -508,7 +514,13 @@ export const TemplatesPanel: React.FC<TemplatesPanelProps> = ({ onDragStart }) =
variant={selectedCategory === category.id ? "default" : "outline"}
size="sm"
onClick={() => setSelectedCategory(category.id)}
className="flex items-center space-x-1"
className={`
flex items-center space-x-1.5 h-8 px-3 text-xs font-medium rounded-full transition-all
${selectedCategory === category.id
? 'bg-blue-600 text-white shadow-sm hover:bg-blue-700'
: 'bg-white/60 text-gray-600 border-gray-200/60 hover:bg-white hover:text-gray-900 hover:border-gray-300'
}
`}
>
{category.icon}
<span>{category.name}</span>
@ -517,23 +529,21 @@ export const TemplatesPanel: React.FC<TemplatesPanelProps> = ({ onDragStart }) =
</div>
</div>
<Separator />
{/* 새로고침 버튼 */}
{error && (
<div className="flex items-center justify-between rounded-lg bg-yellow-50 p-3 text-yellow-800">
<div className="flex items-center justify-between rounded-xl bg-amber-50/80 border border-amber-200/60 p-3 text-amber-800 mb-4">
<div className="flex items-center space-x-2">
<Info className="h-4 w-4" />
<span className="text-sm">릿 , 릿 </span>
</div>
<Button size="sm" variant="outline" onClick={() => refetch()}>
<Button size="sm" variant="outline" onClick={() => refetch()} className="border-amber-300 text-amber-700 hover:bg-amber-100">
<RefreshCw className="h-4 w-4" />
</Button>
</div>
)}
{/* 템플릿 목록 */}
<div className="flex-1 space-y-2 overflow-y-auto">
<div className="flex-1 space-y-3 overflow-y-auto mt-6">
{isLoading ? (
<div className="flex h-32 items-center justify-center">
<LoadingSpinner />
@ -541,9 +551,10 @@ export const TemplatesPanel: React.FC<TemplatesPanelProps> = ({ onDragStart }) =
</div>
) : filteredTemplates.length === 0 ? (
<div className="flex h-32 items-center justify-center text-center text-gray-500">
<div>
<FileText className="mx-auto mb-2 h-8 w-8" />
<p className="text-sm"> </p>
<div className="p-8">
<FileText className="mx-auto mb-3 h-12 w-12 text-gray-300" />
<p className="text-sm font-medium text-gray-600">릿 </p>
<p className="text-xs text-gray-400 mt-1"> </p>
</div>
</div>
) : (
@ -551,27 +562,40 @@ export const TemplatesPanel: React.FC<TemplatesPanelProps> = ({ onDragStart }) =
<div
key={template.id}
draggable
onDragStart={(e) => onDragStart(e, template)}
className="group cursor-move rounded-lg border border-gray-200 bg-white p-3 shadow-sm transition-all hover:border-blue-300 hover:shadow-md"
onDragStart={(e) => {
onDragStart(e, template);
// 드래그 시작 시 시각적 피드백
e.currentTarget.style.opacity = '0.6';
e.currentTarget.style.transform = 'rotate(2deg) scale(0.98)';
}}
onDragEnd={(e) => {
// 드래그 종료 시 원래 상태로 복원
e.currentTarget.style.opacity = '1';
e.currentTarget.style.transform = 'none';
}}
className="group cursor-grab rounded-lg border border-gray-200/40 bg-white/90 backdrop-blur-sm p-5 shadow-sm transition-all duration-300 hover:bg-white hover:shadow-lg hover:shadow-blue-500/15 hover:scale-[1.02] hover:border-blue-300/60 hover:-translate-y-1 active:cursor-grabbing active:scale-[0.98] active:translate-y-0"
>
<div className="flex items-start space-x-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-blue-50 text-blue-600 group-hover:bg-blue-100">
<div className="flex items-start space-x-4">
<div className="flex h-14 w-14 items-center justify-center rounded-xl bg-gradient-to-br from-blue-500 to-blue-600 text-white shadow-md group-hover:shadow-lg group-hover:scale-110 transition-all duration-300">
{template.icon}
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center space-x-2">
<h4 className="truncate font-medium text-gray-900">{template.name}</h4>
<Badge variant="secondary" className="text-xs">
{template.components.length}
<div className="flex items-start justify-between mb-2">
<h4 className="font-semibold text-gray-900 text-sm leading-tight">{template.name}</h4>
<Badge variant="secondary" className="text-xs bg-blue-100 text-blue-700 border-0 ml-2 px-2 py-1 rounded-full font-medium">
{template.components.length}
</Badge>
</div>
<p className="mt-1 line-clamp-2 text-xs text-gray-500">{template.description}</p>
<div className="mt-2 flex items-center space-x-2 text-xs text-gray-400">
<span>
{template.defaultSize.width}×{template.defaultSize.height}
<p className="text-xs text-gray-500 line-clamp-2 leading-relaxed mb-3">{template.description}</p>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2 text-xs text-gray-400">
<span className="bg-gradient-to-r from-gray-100 to-gray-200 px-3 py-1 rounded-full font-medium text-gray-600">
{template.defaultSize.width}×{template.defaultSize.height}
</span>
</div>
<span className="text-xs font-medium text-blue-600 capitalize bg-gradient-to-r from-blue-50 to-indigo-50 px-3 py-1 rounded-full border border-blue-200/50">
{template.category}
</span>
<span></span>
<span className="capitalize">{template.category}</span>
</div>
</div>
</div>
@ -581,12 +605,14 @@ export const TemplatesPanel: React.FC<TemplatesPanelProps> = ({ onDragStart }) =
</div>
{/* 도움말 */}
<div className="rounded-lg bg-blue-50 p-3">
<div className="flex items-start space-x-2">
<Info className="mt-0.5 h-4 w-4 flex-shrink-0 text-blue-600" />
<div className="text-xs text-blue-700">
<p className="mb-1 font-medium"> </p>
<p>릿 .</p>
<div className="rounded-xl bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-100/60 p-4 mt-6">
<div className="flex items-start space-x-3">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-blue-100 text-blue-600">
<Info className="h-4 w-4" />
</div>
<div className="text-xs text-blue-800">
<p className="font-semibold mb-1"> </p>
<p className="text-blue-600 leading-relaxed">릿 .</p>
</div>
</div>
</div>

View File

@ -504,6 +504,26 @@ export function FileUpload({ component, onUpdateComponent, onFileUpload, userInf
[component.id]: updatedFiles
}));
// RealtimePreview 동기화를 위한 추가 이벤트 발생
if (typeof window !== 'undefined') {
const eventDetail = {
componentId: component.id,
files: updatedFiles,
fileCount: updatedFiles.length,
action: 'upload',
timestamp: Date.now()
};
console.log("🚀 FileUpload 위젯 이벤트 발생:", eventDetail);
const event = new CustomEvent('globalFileStateChanged', {
detail: eventDetail
});
window.dispatchEvent(event);
console.log("✅ FileUpload globalFileStateChanged 이벤트 발생 완료");
}
// 컴포넌트 업데이트 (옵셔널)
if (onUpdateComponent) {
onUpdateComponent({
@ -583,6 +603,42 @@ export function FileUpload({ component, onUpdateComponent, onFileUpload, userInf
[component.id]: filteredFiles
}));
// 🎯 화면설계 모드와 동기화를 위한 전역 이벤트 발생
if (typeof window !== 'undefined') {
const eventDetail = {
componentId: component.id,
files: filteredFiles,
fileCount: filteredFiles.length,
action: 'delete',
timestamp: Date.now(),
source: 'realScreen' // 실제 화면에서 온 이벤트임을 표시
};
console.log("🚀🚀🚀 FileUpload 위젯 삭제 이벤트 발생:", eventDetail);
const event = new CustomEvent('globalFileStateChanged', {
detail: eventDetail
});
window.dispatchEvent(event);
console.log("✅✅✅ FileUpload 위젯 → 화면설계 모드 동기화 이벤트 발생 완료");
// 추가 지연 이벤트들
setTimeout(() => {
console.log("🔄 FileUpload 위젯 추가 삭제 이벤트 발생 (지연 100ms)");
window.dispatchEvent(new CustomEvent('globalFileStateChanged', {
detail: { ...eventDetail, delayed: true }
}));
}, 100);
setTimeout(() => {
console.log("🔄 FileUpload 위젯 추가 삭제 이벤트 발생 (지연 300ms)");
window.dispatchEvent(new CustomEvent('globalFileStateChanged', {
detail: { ...eventDetail, delayed: true, attempt: 2 }
}));
}, 300);
}
onUpdateComponent({
uploadedFiles: filteredFiles,
});
@ -635,8 +691,8 @@ export function FileUpload({ component, onUpdateComponent, onFileUpload, userInf
<div className="w-full space-y-4">
{/* 드래그 앤 드롭 영역 */}
<div
className={`rounded-lg border-2 border-dashed p-6 text-center transition-colors ${
isDragOver ? "border-blue-500 bg-blue-50" : "border-gray-300 hover:border-gray-400"
className={`rounded-xl border-2 border-dashed p-8 text-center transition-all duration-300 ${
isDragOver ? "border-blue-400 bg-gradient-to-br from-blue-50 to-indigo-50 shadow-sm" : "border-gray-300/60 hover:border-blue-400/60 hover:bg-gray-50/50 hover:shadow-sm"
}`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
@ -648,7 +704,7 @@ export function FileUpload({ component, onUpdateComponent, onFileUpload, userInf
</p>
<p className="mb-4 text-sm text-gray-500"> </p>
<Button variant="outline" onClick={handleFileInputClick} className="mb-4">
<Button variant="outline" onClick={handleFileInputClick} className="mb-4 rounded-lg border-gray-300/60 hover:border-blue-400/60 hover:bg-blue-50/50 transition-all duration-200">
<Upload className="mr-2 h-4 w-4" />
{fileConfig.uploadButtonText || "파일 선택"}
</Button>

View File

@ -134,7 +134,7 @@ export const FileWidget: React.FC<WebTypeComponentProps> = ({ component, value,
<div className="h-full w-full space-y-2">
{/* 파일 업로드 영역 */}
<div
className="border-muted-foreground/25 hover:border-muted-foreground/50 cursor-pointer rounded-lg border-2 border-dashed p-4 text-center transition-colors"
className="border-gray-300/60 hover:border-blue-400/60 hover:bg-gray-50/50 cursor-pointer rounded-xl border-2 border-dashed p-6 text-center transition-all duration-300 hover:shadow-sm"
onClick={handleFileSelect}
onDragOver={handleDragOver}
onDrop={handleDrop}

View File

@ -244,33 +244,39 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
);
}
// 컨테이너 스타일 (원래 카드 레이아웃과 완전히 동일)
// 컨테이너 스타일 - 통일된 디자인 시스템 적용
const containerStyle: React.CSSProperties = {
display: "grid",
gridTemplateColumns: `repeat(${componentConfig.cardsPerRow || 3}, 1fr)`, // 기본값 3 (한 행당 카드 수)
gridAutoRows: "min-content", // 자동 행 생성으로 모든 데이터 표시
gap: `${componentConfig.cardSpacing || 16}px`,
padding: "16px",
gap: `${componentConfig.cardSpacing || 32}px`, // 간격 대폭 증가로 여유로운 느낌
padding: "32px", // 패딩 대폭 증가
width: "100%",
height: "100%",
background: "transparent",
background: "linear-gradient(to br, #f8fafc, #f1f5f9)", // 부드러운 그라데이션 배경
overflow: "auto",
borderRadius: "12px", // 컨테이너 자체도 라운드 처리
};
// 카드 스타일 (원래 카드 레이아웃과 완전히 동일)
// 카드 스타일 - 통일된 디자인 시스템 적용
const cardStyle: React.CSSProperties = {
backgroundColor: "white",
border: "1px solid #e5e7eb",
borderRadius: "8px",
padding: "16px",
boxShadow: "0 1px 3px 0 rgba(0, 0, 0, 0.1)",
transition: "all 0.2s ease-in-out",
border: "1px solid #e2e8f0", // 더 부드러운 보더 색상
borderRadius: "12px", // 통일된 라운드 처리
padding: "24px", // 더 여유로운 패딩
boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)", // 더 깊은 그림자
transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)", // 부드러운 트랜지션
overflow: "hidden",
display: "flex",
flexDirection: "column",
position: "relative",
minHeight: "200px",
minHeight: "240px", // 최소 높이 더 증가
cursor: isDesignMode ? "pointer" : "default",
// 호버 효과를 위한 추가 스타일
"&:hover": {
transform: "translateY(-2px)",
boxShadow: "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)",
}
};
// 텍스트 자르기 함수
@ -386,53 +392,53 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
<div
key={data.id || index}
style={cardStyle}
className="card-hover"
className="group cursor-pointer hover:transform hover:-translate-y-1 hover:shadow-xl transition-all duration-300 ease-out"
onClick={() => handleCardClick(data)}
>
{/* 카드 이미지 */}
{/* 카드 이미지 - 통일된 디자인 */}
{componentConfig.cardStyle?.showImage && componentConfig.columnMapping?.imageColumn && (
<div className="mb-3 flex justify-center">
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-gray-200">
<span className="text-xl text-gray-500">👤</span>
<div className="mb-4 flex justify-center">
<div className="flex h-20 w-20 items-center justify-center rounded-full bg-gradient-to-br from-blue-100 to-indigo-100 shadow-sm border-2 border-white">
<span className="text-2xl text-blue-600">👤</span>
</div>
</div>
)}
{/* 카드 타이틀 */}
{/* 카드 타이틀 - 통일된 디자인 */}
{componentConfig.cardStyle?.showTitle && (
<div className="mb-2">
<h3 className="text-lg font-semibold text-gray-900">{titleValue}</h3>
<div className="mb-3">
<h3 className="text-xl font-bold text-gray-900 leading-tight">{titleValue}</h3>
</div>
)}
{/* 카드 서브타이틀 */}
{/* 카드 서브타이틀 - 통일된 디자인 */}
{componentConfig.cardStyle?.showSubtitle && (
<div className="mb-2">
<p className="text-sm font-medium text-blue-600">{subtitleValue}</p>
<div className="mb-3">
<p className="text-sm font-semibold text-blue-600 bg-blue-50 px-3 py-1 rounded-full inline-block">{subtitleValue}</p>
</div>
)}
{/* 카드 설명 */}
{/* 카드 설명 - 통일된 디자인 */}
{componentConfig.cardStyle?.showDescription && (
<div className="mb-3 flex-1">
<p className="text-sm leading-relaxed text-gray-600">
<div className="mb-4 flex-1">
<p className="text-sm leading-relaxed text-gray-700 bg-gray-50 p-3 rounded-lg">
{truncateText(descriptionValue, componentConfig.cardStyle?.maxDescriptionLength || 100)}
</p>
</div>
)}
{/* 추가 표시 컬럼들 */}
{/* 추가 표시 컬럼들 - 통일된 디자인 */}
{componentConfig.columnMapping?.displayColumns &&
componentConfig.columnMapping.displayColumns.length > 0 && (
<div className="space-y-1 border-t border-gray-100 pt-3">
<div className="space-y-2 border-t border-gray-200 pt-4">
{componentConfig.columnMapping.displayColumns.map((columnName, idx) => {
const value = getColumnValue(data, columnName);
if (!value) return null;
return (
<div key={idx} className="flex justify-between text-xs">
<span className="text-gray-500 capitalize">{getColumnLabel(columnName)}:</span>
<span className="font-medium text-gray-700">{value}</span>
<div key={idx} className="flex justify-between items-center text-sm bg-white/50 px-3 py-2 rounded-lg border border-gray-100">
<span className="text-gray-600 font-medium capitalize">{getColumnLabel(columnName)}:</span>
<span className="font-semibold text-gray-900 bg-gray-100 px-2 py-1 rounded-md text-xs">{value}</span>
</div>
);
})}

View File

@ -131,15 +131,90 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
}
}, [component.id]); // component.id가 변경될 때만 실행
// 🎯 화면설계 모드에서 실제 화면으로의 실시간 동기화 이벤트 리스너
useEffect(() => {
const handleDesignModeFileChange = (event: CustomEvent) => {
console.log("🎯🎯🎯 FileUploadComponent 화면설계 모드 파일 변경 이벤트 수신:", {
eventComponentId: event.detail.componentId,
currentComponentId: component.id,
isMatch: event.detail.componentId === component.id,
filesCount: event.detail.files?.length || 0,
action: event.detail.action,
source: event.detail.source,
eventDetail: event.detail
});
// 현재 컴포넌트와 일치하고 화면설계 모드에서 온 이벤트인 경우
if (event.detail.componentId === component.id && event.detail.source === 'designMode') {
console.log("✅✅✅ 화면설계 모드 → 실제 화면 파일 동기화 시작:", {
componentId: component.id,
filesCount: event.detail.files?.length || 0,
action: event.detail.action
});
// 파일 상태 업데이트
const newFiles = event.detail.files || [];
setUploadedFiles(newFiles);
// localStorage 백업 업데이트
try {
const backupKey = `fileUpload_${component.id}`;
localStorage.setItem(backupKey, JSON.stringify(newFiles));
console.log("💾 화면설계 모드 동기화 후 localStorage 백업 업데이트:", {
componentId: component.id,
fileCount: newFiles.length
});
} catch (e) {
console.warn("localStorage 백업 업데이트 실패:", e);
}
// 전역 상태 업데이트
if (typeof window !== 'undefined') {
(window as any).globalFileState = {
...(window as any).globalFileState,
[component.id]: newFiles
};
}
// onUpdate 콜백 호출 (부모 컴포넌트에 알림)
if (onUpdate) {
onUpdate({
uploadedFiles: newFiles,
lastFileUpdate: event.detail.timestamp
});
}
console.log("🎉🎉🎉 화면설계 모드 → 실제 화면 동기화 완료:", {
componentId: component.id,
finalFileCount: newFiles.length
});
}
};
if (typeof window !== 'undefined') {
window.addEventListener('globalFileStateChanged', handleDesignModeFileChange as EventListener);
return () => {
window.removeEventListener('globalFileStateChanged', handleDesignModeFileChange as EventListener);
};
}
}, [component.id, onUpdate]);
// 템플릿 파일과 데이터 파일을 조회하는 함수
const loadComponentFiles = useCallback(async () => {
if (!component?.id) return;
try {
const screenId = formData?.screenId || (typeof window !== 'undefined' && window.location.pathname.includes('/screens/')
let screenId = formData?.screenId || (typeof window !== 'undefined' && window.location.pathname.includes('/screens/')
? parseInt(window.location.pathname.split('/screens/')[1])
: null);
// 디자인 모드인 경우 기본 화면 ID 사용
if (!screenId && isDesignMode) {
screenId = 40; // 기본 화면 ID
console.log("📂 디자인 모드: 기본 화면 ID 사용 (40)");
}
if (!screenId) {
console.log("📂 화면 ID 없음, 기존 파일 로직 사용");
return false; // 기존 로직 사용
@ -474,14 +549,18 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
}
const uploadData = {
tableName: tableName,
fieldName: columnName,
recordId: recordId || `temp_${component.id}`,
// 🎯 formData에서 백엔드 API 설정 가져오기
autoLink: formData?.autoLink || true,
linkedTable: formData?.linkedTable || tableName,
recordId: formData?.recordId || recordId || `temp_${component.id}`,
columnName: formData?.columnName || columnName,
isVirtualFileColumn: formData?.isVirtualFileColumn || true,
docType: component.fileConfig?.docType || 'DOCUMENT',
docTypeName: component.fileConfig?.docTypeName || '일반 문서',
// 호환성을 위한 기존 필드들
tableName: tableName,
fieldName: columnName,
targetObjid: targetObjid, // InteractiveDataTable 호환을 위한 targetObjid 추가
columnName: columnName, // 가상 파일 컬럼 지원
isVirtualFileColumn: true, // 가상 파일 컬럼으로 처리
};
console.log("📤 파일 업로드 시작:", {
@ -715,7 +794,9 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
componentId: component.id,
files: updatedFiles,
fileCount: updatedFiles.length,
timestamp: Date.now()
timestamp: Date.now(),
source: 'realScreen', // 🎯 실제 화면에서 온 이벤트임을 표시
action: 'delete'
}
});
window.dispatchEvent(syncEvent);

View File

@ -516,7 +516,7 @@ export const FileViewerModal: React.FC<FileViewerModalProps> = ({
return (
<Dialog open={isOpen} onOpenChange={() => {}}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto [&>button]:hidden">
<DialogHeader>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">

View File

@ -46,7 +46,7 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
return (
<div
className="relative h-full overflow-auto"
className="relative h-full overflow-auto rounded-xl border border-gray-200/60 bg-gradient-to-br from-white to-gray-50/30 shadow-sm"
style={{
width: "100%",
maxWidth: "100%",
@ -62,8 +62,8 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
boxSizing: "border-box",
}}
>
<TableHeader className={tableConfig.stickyHeader ? "sticky top-0 z-20 bg-white" : ""}>
<TableRow>
<TableHeader className={tableConfig.stickyHeader ? "sticky top-0 z-20 bg-gradient-to-r from-slate-50 to-blue-50/30 border-b border-gray-200/60" : "bg-gradient-to-r from-slate-50 to-blue-50/30 border-b border-gray-200/60"}>
<TableRow className="border-b border-gray-200/40">
{visibleColumns.map((column, colIndex) => {
// 왼쪽 고정 컬럼들의 누적 너비 계산
const leftFixedWidth = visibleColumns
@ -84,13 +84,13 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
key={column.columnName}
className={cn(
column.columnName === "__checkbox__"
? "h-10 border-b px-4 py-2 text-center align-middle"
: "h-10 cursor-pointer border-b px-4 py-2 text-left align-middle font-medium whitespace-nowrap text-gray-900 select-none",
? "h-12 border-0 px-4 py-3 text-center align-middle"
: "h-12 cursor-pointer border-0 px-4 py-3 text-left align-middle font-semibold whitespace-nowrap text-slate-700 select-none transition-all duration-200",
`text-${column.align}`,
column.sortable && "hover:bg-gray-50",
column.sortable && "hover:bg-blue-50/50 hover:text-blue-700",
// 고정 컬럼 스타일
column.fixed === "left" && "sticky z-10 border-r bg-white shadow-sm",
column.fixed === "right" && "sticky z-10 border-l bg-white shadow-sm",
column.fixed === "left" && "sticky z-10 border-r border-gray-200/60 bg-gradient-to-r from-slate-50 to-blue-50/30 shadow-sm",
column.fixed === "right" && "sticky z-10 border-l border-gray-200/60 bg-gradient-to-r from-slate-50 to-blue-50/30 shadow-sm",
// 숨김 컬럼 스타일 (디자인 모드에서만)
isDesignMode && column.hidden && "bg-gray-100/50 opacity-40",
)}
@ -118,15 +118,15 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
{columnLabels[column.columnName] || column.displayName || column.columnName}
</span>
{column.sortable && (
<span className="ml-1">
<span className="ml-2 flex h-5 w-5 items-center justify-center rounded-md bg-white/50 shadow-sm">
{sortColumn === column.columnName ? (
sortDirection === "asc" ? (
<ArrowUp className="h-3 w-3 text-blue-600" />
<ArrowUp className="h-3.5 w-3.5 text-blue-600" />
) : (
<ArrowDown className="h-3 w-3 text-blue-600" />
<ArrowDown className="h-3.5 w-3.5 text-blue-600" />
)
) : (
<ArrowUpDown className="h-3 w-3 text-gray-400" />
<ArrowUpDown className="h-3.5 w-3.5 text-gray-400" />
)}
</span>
)}
@ -142,8 +142,16 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
<TableBody>
{data.length === 0 ? (
<TableRow>
<TableCell colSpan={visibleColumns.length} className="py-8 text-center text-gray-500">
<TableCell colSpan={visibleColumns.length} className="py-12 text-center">
<div className="flex flex-col items-center justify-center space-y-3">
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-gradient-to-br from-gray-100 to-gray-200">
<svg className="h-6 w-6 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<span className="text-sm font-medium text-gray-500"> </span>
<span className="text-xs text-gray-400 bg-gray-100 px-3 py-1 rounded-full"> </span>
</div>
</TableCell>
</TableRow>
) : (
@ -151,11 +159,11 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
<TableRow
key={`row-${index}`}
className={cn(
"h-10 cursor-pointer border-b leading-none",
tableConfig.tableStyle?.hoverEffect && "hover:bg-gray-50",
tableConfig.tableStyle?.alternateRows && index % 2 === 1 && "bg-gray-50/30",
"h-12 cursor-pointer border-b border-gray-100/60 leading-none transition-all duration-200",
tableConfig.tableStyle?.hoverEffect && "hover:bg-gradient-to-r hover:from-blue-50/30 hover:to-indigo-50/20 hover:shadow-sm",
tableConfig.tableStyle?.alternateRows && index % 2 === 1 && "bg-gradient-to-r from-slate-50/30 to-gray-50/20",
)}
style={{ minHeight: "40px", height: "40px", lineHeight: "1" }}
style={{ minHeight: "48px", height: "48px", lineHeight: "1" }}
onClick={() => handleRowClick(row)}
>
{visibleColumns.map((column, colIndex) => {
@ -177,15 +185,15 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
<TableCell
key={`cell-${column.columnName}`}
className={cn(
"h-10 px-4 py-2 align-middle text-sm whitespace-nowrap",
"h-12 px-4 py-3 align-middle text-sm whitespace-nowrap text-slate-600 transition-all duration-200",
`text-${column.align}`,
// 고정 컬럼 스타일
column.fixed === "left" && "sticky z-10 border-r bg-white",
column.fixed === "right" && "sticky z-10 border-l bg-white",
column.fixed === "left" && "sticky z-10 border-r border-gray-200/60 bg-white/90 backdrop-blur-sm",
column.fixed === "right" && "sticky z-10 border-l border-gray-200/60 bg-white/90 backdrop-blur-sm",
)}
style={{
minHeight: "40px",
height: "40px",
minHeight: "48px",
height: "48px",
verticalAlign: "middle",
width: getColumnWidth(column),
boxSizing: "border-box",

View File

@ -50,9 +50,10 @@ export abstract class BaseLayoutRenderer extends React.Component<LayoutRendererP
const zoneStyle: React.CSSProperties = {
position: "relative",
// 구역 경계 시각화 - 항상 표시
border: "1px solid #e2e8f0",
borderRadius: "6px",
backgroundColor: "rgba(248, 250, 252, 0.5)",
border: "1px solid rgba(226, 232, 240, 0.6)",
borderRadius: "12px",
backgroundColor: "rgba(248, 250, 252, 0.3)",
boxShadow: "0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)",
transition: "all 0.2s ease",
...this.getZoneStyle(zone),
...additionalProps.style,
@ -62,19 +63,21 @@ export abstract class BaseLayoutRenderer extends React.Component<LayoutRendererP
if (isDesignMode) {
// 🎯 컴포넌트가 있는 존은 테두리 제거 (컴포넌트 자체 테두리와 충돌 방지)
if (zoneChildren.length === 0) {
zoneStyle.border = "2px dashed #cbd5e1";
zoneStyle.backgroundColor = "rgba(241, 245, 249, 0.8)";
zoneStyle.border = "2px dashed rgba(203, 213, 225, 0.6)";
zoneStyle.backgroundColor = "rgba(241, 245, 249, 0.5)";
zoneStyle.borderRadius = "12px";
} else {
// 컴포넌트가 있는 존은 미묘한 배경만
zoneStyle.border = "1px solid transparent";
zoneStyle.backgroundColor = "rgba(248, 250, 252, 0.3)";
zoneStyle.border = "1px solid rgba(226, 232, 240, 0.3)";
zoneStyle.backgroundColor = "rgba(248, 250, 252, 0.2)";
zoneStyle.borderRadius = "12px";
}
}
// 호버 효과를 위한 추가 스타일
const dropZoneStyle: React.CSSProperties = {
minHeight: isDesignMode ? "60px" : "40px",
borderRadius: "4px",
minHeight: isDesignMode ? "80px" : "50px",
borderRadius: "12px",
display: "flex",
flexDirection: "column",
alignItems: zoneChildren.length === 0 ? "center" : "stretch",