ui개선
This commit is contained in:
parent
2a8081a253
commit
2e916678fa
|
|
@ -227,7 +227,7 @@ export const FloatingPanel: React.FC<FloatingPanelProps> = ({
|
||||||
<div
|
<div
|
||||||
ref={panelRef}
|
ref={panelRef}
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed z-[100] rounded-xl border border-gray-200/60 bg-white/95 backdrop-blur-sm shadow-xl shadow-gray-900/10",
|
"bg-card text-card-foreground fixed z-[100] rounded-lg border shadow-lg",
|
||||||
isDragging ? "cursor-move shadow-2xl" : "transition-all duration-200 ease-in-out",
|
isDragging ? "cursor-move shadow-2xl" : "transition-all duration-200 ease-in-out",
|
||||||
isResizing && "cursor-se-resize",
|
isResizing && "cursor-se-resize",
|
||||||
className,
|
className,
|
||||||
|
|
@ -239,28 +239,28 @@ export const FloatingPanel: React.FC<FloatingPanelProps> = ({
|
||||||
height: `${panelSize.height}px`,
|
height: `${panelSize.height}px`,
|
||||||
transform: isDragging ? "scale(1.01)" : "scale(1)",
|
transform: isDragging ? "scale(1.01)" : "scale(1)",
|
||||||
transition: isDragging ? "none" : "transform 0.1s ease-out, box-shadow 0.1s ease-out",
|
transition: isDragging ? "none" : "transform 0.1s ease-out, box-shadow 0.1s ease-out",
|
||||||
zIndex: isDragging ? 101 : 100, // 항상 컴포넌트보다 위에 표시
|
zIndex: isDragging ? 101 : 100,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
<div
|
<div
|
||||||
ref={dragHandleRef}
|
ref={dragHandleRef}
|
||||||
data-header="true"
|
data-header="true"
|
||||||
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"
|
className="bg-muted/40 flex cursor-move items-center justify-between border-b px-4 py-3"
|
||||||
onMouseDown={handleDragStart}
|
onMouseDown={handleDragStart}
|
||||||
style={{
|
style={{
|
||||||
userSelect: "none", // 텍스트 선택 방지
|
userSelect: "none",
|
||||||
WebkitUserSelect: "none",
|
WebkitUserSelect: "none",
|
||||||
MozUserSelect: "none",
|
MozUserSelect: "none",
|
||||||
msUserSelect: "none",
|
msUserSelect: "none",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center gap-2">
|
||||||
<GripVertical className="h-4 w-4 text-gray-400" />
|
<GripVertical className="text-muted-foreground h-4 w-4" />
|
||||||
<h3 className="text-sm font-medium text-gray-900">{title}</h3>
|
<h3 className="text-sm font-semibold">{title}</h3>
|
||||||
</div>
|
</div>
|
||||||
<button onClick={onClose} className="rounded-lg p-2 transition-all duration-200 hover:bg-white/80 hover:shadow-sm">
|
<button onClick={onClose} className="hover:bg-accent rounded-md p-1.5 transition-colors">
|
||||||
<X className="h-4 w-4 text-gray-500 hover:text-gray-700" />
|
<X className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -282,7 +282,7 @@ export const FloatingPanel: React.FC<FloatingPanelProps> = ({
|
||||||
{/* 리사이즈 핸들 */}
|
{/* 리사이즈 핸들 */}
|
||||||
{resizable && !autoHeight && (
|
{resizable && !autoHeight && (
|
||||||
<div className="absolute right-0 bottom-0 h-4 w-4 cursor-se-resize" onMouseDown={handleResizeStart}>
|
<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-gradient-to-br from-gray-400 to-gray-500 shadow-sm" />
|
<div className="bg-muted-foreground/40 absolute right-1 bottom-1 h-2 w-2 rounded-sm" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -83,9 +83,9 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||||
// 선택 상태에 따른 스타일 (z-index 낮춤 - 패널과 모달보다 아래)
|
// 선택 상태에 따른 스타일 (z-index 낮춤 - 패널과 모달보다 아래)
|
||||||
const selectionStyle = isSelected
|
const selectionStyle = isSelected
|
||||||
? {
|
? {
|
||||||
outline: "2px solid #3b82f6",
|
outline: "2px solid hsl(var(--primary))",
|
||||||
outlineOffset: "2px",
|
outlineOffset: "2px",
|
||||||
zIndex: 20, // 패널과 모달보다 낮게 설정
|
zIndex: 20,
|
||||||
}
|
}
|
||||||
: {};
|
: {};
|
||||||
|
|
||||||
|
|
@ -183,16 +183,16 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||||
|
|
||||||
{/* 선택된 컴포넌트 정보 표시 */}
|
{/* 선택된 컴포넌트 정보 표시 */}
|
||||||
{isSelected && (
|
{isSelected && (
|
||||||
<div className="absolute -top-8 left-0 rounded-lg bg-gray-800/90 px-3 py-2 text-xs text-white shadow-lg backdrop-blur-sm">
|
<div className="bg-primary text-primary-foreground absolute -top-7 left-0 rounded-md px-2.5 py-1 text-xs font-medium shadow-sm">
|
||||||
{type === "widget" && (
|
{type === "widget" && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-1.5">
|
||||||
{getWidgetIcon((component as WidgetComponent).widgetType)}
|
{getWidgetIcon((component as WidgetComponent).widgetType)}
|
||||||
<span className="font-medium">{(component as WidgetComponent).widgetType || "widget"}</span>
|
<span>{(component as WidgetComponent).widgetType || "widget"}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{type !== "widget" && (
|
{type !== "widget" && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-1.5">
|
||||||
<span className="font-medium">{component.componentConfig?.type || type}</span>
|
<span>{component.componentConfig?.type || type}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -28,79 +28,91 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`space-y-6 p-4 ${className}`}>
|
<div className={`space-y-6 p-6 ${className}`}>
|
||||||
{/* 여백 섹션 */}
|
{/* 여백 섹션 */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Box className="h-4 w-4 text-blue-600" />
|
<Box className="text-primary h-4 w-4" />
|
||||||
<h3 className="font-semibold text-gray-900">여백</h3>
|
<h3 className="text-sm font-semibold">여백</h3>
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator className="my-2" />
|
||||||
<div className="space-y-4">
|
<div className="space-y-3">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<div className="space-y-2">
|
<div className="space-y-1.5">
|
||||||
<Label htmlFor="margin">외부 여백</Label>
|
<Label htmlFor="margin" className="text-xs font-medium">
|
||||||
|
외부 여백
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="margin"
|
id="margin"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="10px, 1rem"
|
placeholder="10px"
|
||||||
value={localStyle.margin || ""}
|
value={localStyle.margin || ""}
|
||||||
onChange={(e) => handleStyleChange("margin", e.target.value)}
|
onChange={(e) => handleStyleChange("margin", e.target.value)}
|
||||||
|
className="h-8"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-1.5">
|
||||||
<Label htmlFor="padding">내부 여백</Label>
|
<Label htmlFor="padding" className="text-xs font-medium">
|
||||||
|
내부 여백
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="padding"
|
id="padding"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="10px, 1rem"
|
placeholder="10px"
|
||||||
value={localStyle.padding || ""}
|
value={localStyle.padding || ""}
|
||||||
onChange={(e) => handleStyleChange("padding", e.target.value)}
|
onChange={(e) => handleStyleChange("padding", e.target.value)}
|
||||||
|
className="h-8"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="space-y-1.5">
|
||||||
<div className="space-y-2">
|
<Label htmlFor="gap" className="text-xs font-medium">
|
||||||
<Label htmlFor="gap">간격</Label>
|
간격
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="gap"
|
id="gap"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="10px, 1rem"
|
placeholder="10px"
|
||||||
value={localStyle.gap || ""}
|
value={localStyle.gap || ""}
|
||||||
onChange={(e) => handleStyleChange("gap", e.target.value)}
|
onChange={(e) => handleStyleChange("gap", e.target.value)}
|
||||||
|
className="h-8"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 테두리 섹션 */}
|
{/* 테두리 섹션 */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Square className="h-4 w-4 text-green-600" />
|
<Square className="text-primary h-4 w-4" />
|
||||||
<h3 className="font-semibold text-gray-900">테두리</h3>
|
<h3 className="text-sm font-semibold">테두리</h3>
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator className="my-2" />
|
||||||
<div className="space-y-4">
|
<div className="space-y-3">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<div className="space-y-2">
|
<div className="space-y-1.5">
|
||||||
<Label htmlFor="borderWidth">테두리 두께</Label>
|
<Label htmlFor="borderWidth" className="text-xs font-medium">
|
||||||
|
두께
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="borderWidth"
|
id="borderWidth"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="1px, 2px"
|
placeholder="1px"
|
||||||
value={localStyle.borderWidth || ""}
|
value={localStyle.borderWidth || ""}
|
||||||
onChange={(e) => handleStyleChange("borderWidth", e.target.value)}
|
onChange={(e) => handleStyleChange("borderWidth", e.target.value)}
|
||||||
|
className="h-8"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-1.5">
|
||||||
<Label htmlFor="borderStyle">테두리 스타일</Label>
|
<Label htmlFor="borderStyle" className="text-xs font-medium">
|
||||||
|
스타일
|
||||||
|
</Label>
|
||||||
<Select
|
<Select
|
||||||
value={localStyle.borderStyle || "solid"}
|
value={localStyle.borderStyle || "solid"}
|
||||||
onValueChange={(value) => handleStyleChange("borderStyle", value)}
|
onValueChange={(value) => handleStyleChange("borderStyle", value)}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger className="h-8">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
@ -113,24 +125,39 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<div className="space-y-2">
|
<div className="space-y-1.5">
|
||||||
<Label htmlFor="borderColor">테두리 색상</Label>
|
<Label htmlFor="borderColor" className="text-xs font-medium">
|
||||||
|
색상
|
||||||
|
</Label>
|
||||||
|
<div className="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
id="borderColor"
|
id="borderColor"
|
||||||
type="color"
|
type="color"
|
||||||
value={localStyle.borderColor || "#000000"}
|
value={localStyle.borderColor || "#000000"}
|
||||||
onChange={(e) => handleStyleChange("borderColor", e.target.value)}
|
onChange={(e) => handleStyleChange("borderColor", e.target.value)}
|
||||||
|
className="h-8 w-14 p-1"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={localStyle.borderColor || "#000000"}
|
||||||
|
onChange={(e) => handleStyleChange("borderColor", e.target.value)}
|
||||||
|
placeholder="#000000"
|
||||||
|
className="h-8 flex-1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
</div>
|
||||||
<Label htmlFor="borderRadius">모서리 둥글기</Label>
|
<div className="space-y-1.5">
|
||||||
|
<Label htmlFor="borderRadius" className="text-xs font-medium">
|
||||||
|
모서리
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="borderRadius"
|
id="borderRadius"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="5px, 10px"
|
placeholder="5px"
|
||||||
value={localStyle.borderRadius || ""}
|
value={localStyle.borderRadius || ""}
|
||||||
onChange={(e) => handleStyleChange("borderRadius", e.target.value)}
|
onChange={(e) => handleStyleChange("borderRadius", e.target.value)}
|
||||||
|
className="h-8"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -138,74 +165,106 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 배경 섹션 */}
|
{/* 배경 섹션 */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Palette className="h-4 w-4 text-purple-600" />
|
<Palette className="text-primary h-4 w-4" />
|
||||||
<h3 className="font-semibold text-gray-900">배경</h3>
|
<h3 className="text-sm font-semibold">배경</h3>
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator className="my-2" />
|
||||||
<div className="space-y-4">
|
<div className="space-y-3">
|
||||||
<div className="space-y-2">
|
<div className="space-y-1.5">
|
||||||
<Label htmlFor="backgroundColor">배경 색상</Label>
|
<Label htmlFor="backgroundColor" className="text-xs font-medium">
|
||||||
|
배경 색상
|
||||||
|
</Label>
|
||||||
|
<div className="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
id="backgroundColor"
|
id="backgroundColor"
|
||||||
type="color"
|
type="color"
|
||||||
value={localStyle.backgroundColor || "#ffffff"}
|
value={localStyle.backgroundColor || "#ffffff"}
|
||||||
onChange={(e) => handleStyleChange("backgroundColor", e.target.value)}
|
onChange={(e) => handleStyleChange("backgroundColor", e.target.value)}
|
||||||
|
className="h-8 w-14 p-1"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={localStyle.backgroundColor || "#ffffff"}
|
||||||
|
onChange={(e) => handleStyleChange("backgroundColor", e.target.value)}
|
||||||
|
placeholder="#ffffff"
|
||||||
|
className="h-8 flex-1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-1.5">
|
||||||
<Label htmlFor="backgroundImage">배경 이미지</Label>
|
<Label htmlFor="backgroundImage" className="text-xs font-medium">
|
||||||
|
배경 이미지
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="backgroundImage"
|
id="backgroundImage"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="url('image.jpg')"
|
placeholder="url('image.jpg')"
|
||||||
value={localStyle.backgroundImage || ""}
|
value={localStyle.backgroundImage || ""}
|
||||||
onChange={(e) => handleStyleChange("backgroundImage", e.target.value)}
|
onChange={(e) => handleStyleChange("backgroundImage", e.target.value)}
|
||||||
|
className="h-8"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 텍스트 섹션 */}
|
{/* 텍스트 섹션 */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Type className="h-4 w-4 text-orange-600" />
|
<Type className="text-primary h-4 w-4" />
|
||||||
<h3 className="font-semibold text-gray-900">텍스트</h3>
|
<h3 className="text-sm font-semibold">텍스트</h3>
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator className="my-2" />
|
||||||
<div className="space-y-4">
|
<div className="space-y-3">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<div className="space-y-2">
|
<div className="space-y-1.5">
|
||||||
<Label htmlFor="color">텍스트 색상</Label>
|
<Label htmlFor="color" className="text-xs font-medium">
|
||||||
|
색상
|
||||||
|
</Label>
|
||||||
|
<div className="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
id="color"
|
id="color"
|
||||||
type="color"
|
type="color"
|
||||||
value={localStyle.color || "#000000"}
|
value={localStyle.color || "#000000"}
|
||||||
onChange={(e) => handleStyleChange("color", e.target.value)}
|
onChange={(e) => handleStyleChange("color", e.target.value)}
|
||||||
|
className="h-8 w-14 p-1"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={localStyle.color || "#000000"}
|
||||||
|
onChange={(e) => handleStyleChange("color", e.target.value)}
|
||||||
|
placeholder="#000000"
|
||||||
|
className="h-8 flex-1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
</div>
|
||||||
<Label htmlFor="fontSize">글자 크기</Label>
|
<div className="space-y-1.5">
|
||||||
|
<Label htmlFor="fontSize" className="text-xs font-medium">
|
||||||
|
크기
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="fontSize"
|
id="fontSize"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="14px, 1rem"
|
placeholder="14px"
|
||||||
value={localStyle.fontSize || ""}
|
value={localStyle.fontSize || ""}
|
||||||
onChange={(e) => handleStyleChange("fontSize", e.target.value)}
|
onChange={(e) => handleStyleChange("fontSize", e.target.value)}
|
||||||
|
className="h-8"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<div className="space-y-2">
|
<div className="space-y-1.5">
|
||||||
<Label htmlFor="fontWeight">글자 굵기</Label>
|
<Label htmlFor="fontWeight" className="text-xs font-medium">
|
||||||
|
굵기
|
||||||
|
</Label>
|
||||||
<Select
|
<Select
|
||||||
value={localStyle.fontWeight || "normal"}
|
value={localStyle.fontWeight || "normal"}
|
||||||
onValueChange={(value) => handleStyleChange("fontWeight", value)}
|
onValueChange={(value) => handleStyleChange("fontWeight", value)}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger className="h-8">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
@ -219,13 +278,15 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-1.5">
|
||||||
<Label htmlFor="textAlign">텍스트 정렬</Label>
|
<Label htmlFor="textAlign" className="text-xs font-medium">
|
||||||
|
정렬
|
||||||
|
</Label>
|
||||||
<Select
|
<Select
|
||||||
value={localStyle.textAlign || "left"}
|
value={localStyle.textAlign || "left"}
|
||||||
onValueChange={(value) => handleStyleChange("textAlign", value)}
|
onValueChange={(value) => handleStyleChange("textAlign", value)}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger className="h-8">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
|
||||||
|
|
@ -107,17 +107,17 @@ export function ComponentsPanel({ className }: ComponentsPanelProps) {
|
||||||
e.currentTarget.style.opacity = "1";
|
e.currentTarget.style.opacity = "1";
|
||||||
e.currentTarget.style.transform = "none";
|
e.currentTarget.style.transform = "none";
|
||||||
}}
|
}}
|
||||||
className="group cursor-grab rounded-lg border border-gray-200/40 bg-white/90 p-4 shadow-sm backdrop-blur-sm transition-all duration-300 hover:-translate-y-1 hover:scale-[1.02] hover:border-purple-300/60 hover:bg-white hover:shadow-lg hover:shadow-purple-500/15 active:translate-y-0 active:scale-[0.98] active:cursor-grabbing"
|
className="group bg-card hover:border-primary/50 cursor-grab rounded-lg border p-3 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md active:translate-y-0 active:scale-[0.98] active:cursor-grabbing"
|
||||||
>
|
>
|
||||||
<div className="flex items-start space-x-3">
|
<div className="flex items-start gap-3">
|
||||||
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-purple-100 text-purple-700 shadow-md transition-all duration-300 group-hover:scale-110 group-hover:shadow-lg">
|
<div className="bg-primary/10 text-primary group-hover:bg-primary/20 flex h-10 w-10 items-center justify-center rounded-md transition-all duration-200">
|
||||||
{getCategoryIcon(component.category)}
|
{getCategoryIcon(component.category)}
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<h4 className="mb-1 text-sm leading-tight font-semibold text-gray-900">{component.name}</h4>
|
<h4 className="mb-1 text-xs leading-tight font-semibold">{component.name}</h4>
|
||||||
<p className="mb-2 line-clamp-2 text-xs leading-relaxed text-gray-500">{component.description}</p>
|
<p className="text-muted-foreground mb-1.5 line-clamp-2 text-xs leading-relaxed">{component.description}</p>
|
||||||
<div className="flex items-center space-x-2 text-xs text-gray-400">
|
<div className="flex items-center">
|
||||||
<span className="rounded-full bg-purple-100 px-2 py-0.5 font-medium text-purple-700">
|
<span className="bg-muted text-muted-foreground rounded-full px-2 py-0.5 text-xs font-medium">
|
||||||
{component.defaultSize.width}×{component.defaultSize.height}
|
{component.defaultSize.width}×{component.defaultSize.height}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -128,80 +128,80 @@ export function ComponentsPanel({ className }: ComponentsPanelProps) {
|
||||||
|
|
||||||
// 빈 상태 렌더링
|
// 빈 상태 렌더링
|
||||||
const renderEmptyState = () => (
|
const renderEmptyState = () => (
|
||||||
<div className="flex h-32 items-center justify-center text-center text-gray-500">
|
<div className="flex h-32 items-center justify-center text-center">
|
||||||
<div className="p-8">
|
<div className="p-6">
|
||||||
<Package className="mx-auto mb-3 h-12 w-12 text-gray-300" />
|
<Package className="text-muted-foreground/40 mx-auto mb-2 h-10 w-10" />
|
||||||
<p className="text-muted-foreground text-sm font-medium">컴포넌트를 찾을 수 없습니다</p>
|
<p className="text-muted-foreground text-xs font-medium">컴포넌트를 찾을 수 없습니다</p>
|
||||||
<p className="mt-1 text-xs text-gray-400">검색어를 조정해보세요</p>
|
<p className="text-muted-foreground/60 mt-1 text-xs">검색어를 조정해보세요</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`flex h-full flex-col border-r border-gray-200/60 bg-slate-50 p-6 shadow-sm ${className}`}>
|
<div className={`bg-background flex h-full flex-col p-4 ${className}`}>
|
||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
<div className="mb-4">
|
<div className="mb-3">
|
||||||
<h2 className="mb-1 text-lg font-semibold text-gray-900">컴포넌트</h2>
|
<h2 className="mb-0.5 text-sm font-semibold">컴포넌트</h2>
|
||||||
<p className="text-sm text-gray-500">{allComponents.length}개의 사용 가능한 컴포넌트</p>
|
<p className="text-muted-foreground text-xs">{allComponents.length}개 사용 가능</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 검색 */}
|
{/* 검색 */}
|
||||||
<div className="mb-4">
|
<div className="mb-3">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-gray-400" />
|
<Search className="text-muted-foreground absolute top-1/2 left-2.5 h-3.5 w-3.5 -translate-y-1/2" />
|
||||||
<Input
|
<Input
|
||||||
placeholder="컴포넌트 검색..."
|
placeholder="컴포넌트 검색..."
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
className="border-0 bg-white/80 pl-10 shadow-sm backdrop-blur-sm transition-colors focus:bg-white"
|
className="h-8 pl-8 text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 카테고리 탭 */}
|
{/* 카테고리 탭 */}
|
||||||
<Tabs defaultValue="input" className="flex flex-1 flex-col">
|
<Tabs defaultValue="input" className="flex flex-1 flex-col">
|
||||||
<TabsList className="mb-4 grid w-full grid-cols-4 bg-white/80 p-1">
|
<TabsList className="mb-3 grid h-8 w-full grid-cols-4">
|
||||||
<TabsTrigger value="input" className="flex items-center gap-1 text-xs">
|
<TabsTrigger value="input" className="flex items-center gap-1 px-1 text-xs">
|
||||||
<Edit3 className="h-3 w-3" />
|
<Edit3 className="h-3 w-3" />
|
||||||
입력
|
<span className="hidden sm:inline">입력</span>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="action" className="flex items-center gap-1 text-xs">
|
<TabsTrigger value="action" className="flex items-center gap-1 px-1 text-xs">
|
||||||
<Zap className="h-3 w-3" />
|
<Zap className="h-3 w-3" />
|
||||||
액션
|
<span className="hidden sm:inline">액션</span>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="display" className="flex items-center gap-1 text-xs">
|
<TabsTrigger value="display" className="flex items-center gap-1 px-1 text-xs">
|
||||||
<BarChart3 className="h-3 w-3" />
|
<BarChart3 className="h-3 w-3" />
|
||||||
표시
|
<span className="hidden sm:inline">표시</span>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="layout" className="flex items-center gap-1 text-xs">
|
<TabsTrigger value="layout" className="flex items-center gap-1 px-1 text-xs">
|
||||||
<Layers className="h-3 w-3" />
|
<Layers className="h-3 w-3" />
|
||||||
레이아웃
|
<span className="hidden sm:inline">레이아웃</span>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
{/* 입력 컴포넌트 */}
|
{/* 입력 컴포넌트 */}
|
||||||
<TabsContent value="input" className="mt-0 flex-1 space-y-3 overflow-y-auto">
|
<TabsContent value="input" className="mt-0 flex-1 space-y-2 overflow-y-auto">
|
||||||
{getFilteredComponents("input").length > 0
|
{getFilteredComponents("input").length > 0
|
||||||
? getFilteredComponents("input").map(renderComponentCard)
|
? getFilteredComponents("input").map(renderComponentCard)
|
||||||
: renderEmptyState()}
|
: renderEmptyState()}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
{/* 액션 컴포넌트 */}
|
{/* 액션 컴포넌트 */}
|
||||||
<TabsContent value="action" className="mt-0 flex-1 space-y-3 overflow-y-auto">
|
<TabsContent value="action" className="mt-0 flex-1 space-y-2 overflow-y-auto">
|
||||||
{getFilteredComponents("action").length > 0
|
{getFilteredComponents("action").length > 0
|
||||||
? getFilteredComponents("action").map(renderComponentCard)
|
? getFilteredComponents("action").map(renderComponentCard)
|
||||||
: renderEmptyState()}
|
: renderEmptyState()}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
{/* 표시 컴포넌트 */}
|
{/* 표시 컴포넌트 */}
|
||||||
<TabsContent value="display" className="mt-0 flex-1 space-y-3 overflow-y-auto">
|
<TabsContent value="display" className="mt-0 flex-1 space-y-2 overflow-y-auto">
|
||||||
{getFilteredComponents("display").length > 0
|
{getFilteredComponents("display").length > 0
|
||||||
? getFilteredComponents("display").map(renderComponentCard)
|
? getFilteredComponents("display").map(renderComponentCard)
|
||||||
: renderEmptyState()}
|
: renderEmptyState()}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
{/* 레이아웃 컴포넌트 */}
|
{/* 레이아웃 컴포넌트 */}
|
||||||
<TabsContent value="layout" className="mt-0 flex-1 space-y-3 overflow-y-auto">
|
<TabsContent value="layout" className="mt-0 flex-1 space-y-2 overflow-y-auto">
|
||||||
{getFilteredComponents("layout").length > 0
|
{getFilteredComponents("layout").length > 0
|
||||||
? getFilteredComponents("layout").map(renderComponentCard)
|
? getFilteredComponents("layout").map(renderComponentCard)
|
||||||
: renderEmptyState()}
|
: renderEmptyState()}
|
||||||
|
|
@ -209,16 +209,14 @@ export function ComponentsPanel({ className }: ComponentsPanelProps) {
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
{/* 도움말 */}
|
{/* 도움말 */}
|
||||||
<div className="mt-4 rounded-xl border border-purple-100/60 bg-gradient-to-r from-purple-50 to-pink-50 p-4">
|
<div className="border-primary/20 bg-primary/5 mt-3 rounded-lg border p-3">
|
||||||
<div className="flex items-start space-x-3">
|
<div className="flex items-start gap-2">
|
||||||
<MousePointer className="mt-0.5 h-4 w-4 flex-shrink-0 text-purple-600" />
|
<MousePointer className="text-primary mt-0.5 h-3.5 w-3.5 flex-shrink-0" />
|
||||||
<div className="flex-1">
|
<p className="text-muted-foreground text-xs leading-relaxed">
|
||||||
<p className="text-xs leading-relaxed text-gray-700">
|
컴포넌트를 <span className="text-foreground font-semibold">드래그</span>하여 화면에 추가하세요
|
||||||
컴포넌트를 <span className="font-semibold text-purple-700">드래그</span>하여 화면에 추가하세요
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -81,30 +81,30 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col">
|
||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
<div className="border-b border-gray-200 p-4">
|
<div className="border-b p-4">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center gap-2">
|
||||||
<Settings className="text-muted-foreground h-4 w-4" />
|
<Settings className="text-muted-foreground h-4 w-4" />
|
||||||
<h3 className="font-medium text-gray-900">레이아웃 설정</h3>
|
<h3 className="text-sm font-semibold">레이아웃 설정</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 flex items-center space-x-2">
|
<div className="mt-2 flex items-center gap-2">
|
||||||
<span className="text-muted-foreground text-sm">타입:</span>
|
<span className="text-muted-foreground text-xs">타입:</span>
|
||||||
<span className="rounded bg-purple-100 px-2 py-1 text-xs font-medium text-purple-800">
|
<span className="bg-primary/10 text-primary rounded-md px-2 py-0.5 text-xs font-medium">
|
||||||
{layoutComponent.layoutType}
|
{layoutComponent.layoutType}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-xs text-gray-500">ID: {layoutComponent.id}</div>
|
<div className="text-muted-foreground mt-1 text-xs">ID: {layoutComponent.id}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 레이아웃 설정 영역 */}
|
{/* 레이아웃 설정 영역 */}
|
||||||
<div className="flex-1 space-y-4 overflow-y-auto p-4">
|
<div className="flex-1 space-y-3 overflow-y-auto p-4">
|
||||||
{/* 기본 정보 */}
|
{/* 기본 정보 */}
|
||||||
<div>
|
<div className="space-y-1.5">
|
||||||
<label className="mb-2 block text-sm font-medium text-gray-700">레이아웃 이름</label>
|
<label className="text-xs font-medium">레이아웃 이름</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={layoutComponent.label || ""}
|
value={layoutComponent.label || ""}
|
||||||
onChange={(e) => onUpdateProperty(layoutComponent.id, "label", e.target.value)}
|
onChange={(e) => onUpdateProperty(layoutComponent.id, "label", e.target.value)}
|
||||||
className="focus:border-primary w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500"
|
className="border-input bg-background focus-visible:ring-ring h-8 w-full rounded-md border px-3 text-xs focus-visible:ring-1 focus-visible:outline-none"
|
||||||
placeholder="레이아웃 이름을 입력하세요"
|
placeholder="레이아웃 이름을 입력하세요"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -55,44 +55,44 @@ export const GridPanel: React.FC<GridPanelProps> = ({
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col">
|
||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
<div className="border-b border-gray-200 p-4">
|
<div className="border-b p-4">
|
||||||
<div className="mb-3 flex items-center justify-between">
|
<div className="mb-3 flex items-center justify-between">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center gap-2">
|
||||||
<Grid3X3 className="h-4 w-4 text-muted-foreground" />
|
<Grid3X3 className="text-muted-foreground h-4 w-4" />
|
||||||
<h3 className="font-medium text-gray-900">격자 설정</h3>
|
<h3 className="text-sm font-semibold">격자 설정</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center gap-1.5">
|
||||||
{onForceGridUpdate && (
|
{onForceGridUpdate && (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={onForceGridUpdate}
|
onClick={onForceGridUpdate}
|
||||||
className="flex items-center space-x-1"
|
className="h-7 px-2 text-xs"
|
||||||
title="현재 해상도에 맞게 모든 컴포넌트를 격자에 재정렬합니다"
|
title="현재 해상도에 맞게 모든 컴포넌트를 격자에 재정렬합니다"
|
||||||
>
|
>
|
||||||
<RefreshCw className="h-3 w-3" />
|
<RefreshCw className="mr-1 h-3 w-3" />
|
||||||
<span>재정렬</span>
|
재정렬
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button size="sm" variant="outline" onClick={onResetGrid} className="flex items-center space-x-1">
|
<Button size="sm" variant="outline" onClick={onResetGrid} className="h-7 px-2 text-xs">
|
||||||
<RotateCcw className="h-3 w-3" />
|
<RotateCcw className="mr-1 h-3 w-3" />
|
||||||
<span>초기화</span>
|
초기화
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 주요 토글들 */}
|
{/* 주요 토글들 */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-2.5">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center gap-2">
|
||||||
{gridSettings.showGrid ? (
|
{gridSettings.showGrid ? (
|
||||||
<Eye className="h-4 w-4 text-primary" />
|
<Eye className="text-primary h-3.5 w-3.5" />
|
||||||
) : (
|
) : (
|
||||||
<EyeOff className="h-4 w-4 text-gray-400" />
|
<EyeOff className="text-muted-foreground h-3.5 w-3.5" />
|
||||||
)}
|
)}
|
||||||
<Label htmlFor="showGrid" className="text-sm font-medium">
|
<Label htmlFor="showGrid" className="text-xs font-medium">
|
||||||
격자 표시
|
격자 표시
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -104,9 +104,9 @@ export const GridPanel: React.FC<GridPanelProps> = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center gap-2">
|
||||||
<Zap className="h-4 w-4 text-green-600" />
|
<Zap className="text-primary h-3.5 w-3.5" />
|
||||||
<Label htmlFor="snapToGrid" className="text-sm font-medium">
|
<Label htmlFor="snapToGrid" className="text-xs font-medium">
|
||||||
격자 스냅
|
격자 스냅
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -120,14 +120,14 @@ export const GridPanel: React.FC<GridPanelProps> = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 설정 영역 */}
|
{/* 설정 영역 */}
|
||||||
<div className="flex-1 space-y-6 overflow-y-auto p-4">
|
<div className="flex-1 space-y-4 overflow-y-auto p-4">
|
||||||
{/* 격자 구조 */}
|
{/* 격자 구조 */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-3">
|
||||||
<h4 className="font-medium text-gray-900">격자 구조</h4>
|
<h4 className="text-xs font-semibold">격자 구조</h4>
|
||||||
|
|
||||||
<div>
|
<div className="space-y-2">
|
||||||
<Label htmlFor="columns" className="mb-2 block text-sm font-medium">
|
<Label htmlFor="columns" className="text-xs font-medium">
|
||||||
컬럼 수: {gridSettings.columns}
|
컬럼 수: <span className="text-primary">{gridSettings.columns}</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Slider
|
<Slider
|
||||||
id="columns"
|
id="columns"
|
||||||
|
|
@ -138,15 +138,15 @@ export const GridPanel: React.FC<GridPanelProps> = ({
|
||||||
onValueChange={([value]) => updateSetting("columns", value)}
|
onValueChange={([value]) => updateSetting("columns", value)}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
<div className="mt-1 flex justify-between text-xs text-gray-500">
|
<div className="text-muted-foreground flex justify-between text-xs">
|
||||||
<span>1</span>
|
<span>1</span>
|
||||||
<span>24</span>
|
<span>24</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div className="space-y-2">
|
||||||
<Label htmlFor="gap" className="mb-2 block text-sm font-medium">
|
<Label htmlFor="gap" className="text-xs font-medium">
|
||||||
간격: {gridSettings.gap}px
|
간격: <span className="text-primary">{gridSettings.gap}px</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Slider
|
<Slider
|
||||||
id="gap"
|
id="gap"
|
||||||
|
|
@ -157,15 +157,15 @@ export const GridPanel: React.FC<GridPanelProps> = ({
|
||||||
onValueChange={([value]) => updateSetting("gap", value)}
|
onValueChange={([value]) => updateSetting("gap", value)}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
<div className="mt-1 flex justify-between text-xs text-gray-500">
|
<div className="text-muted-foreground flex justify-between text-xs">
|
||||||
<span>0px</span>
|
<span>0px</span>
|
||||||
<span>40px</span>
|
<span>40px</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div className="space-y-2">
|
||||||
<Label htmlFor="padding" className="mb-2 block text-sm font-medium">
|
<Label htmlFor="padding" className="text-xs font-medium">
|
||||||
여백: {gridSettings.padding}px
|
여백: <span className="text-primary">{gridSettings.padding}px</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Slider
|
<Slider
|
||||||
id="padding"
|
id="padding"
|
||||||
|
|
@ -176,7 +176,7 @@ export const GridPanel: React.FC<GridPanelProps> = ({
|
||||||
onValueChange={([value]) => updateSetting("padding", value)}
|
onValueChange={([value]) => updateSetting("padding", value)}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
<div className="mt-1 flex justify-between text-xs text-gray-500">
|
<div className="text-muted-foreground flex justify-between text-xs">
|
||||||
<span>0px</span>
|
<span>0px</span>
|
||||||
<span>60px</span>
|
<span>60px</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -248,8 +248,8 @@ export const GridPanel: React.FC<GridPanelProps> = ({
|
||||||
opacity: gridSettings.gridOpacity || 0.5,
|
opacity: gridSettings.gridOpacity || 0.5,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex h-16 items-center justify-center rounded border-2 border-dashed border-blue-300 bg-primary/20">
|
<div className="bg-primary/20 flex h-16 items-center justify-center rounded border-2 border-dashed border-blue-300">
|
||||||
<span className="text-xs text-primary">컴포넌트 예시</span>
|
<span className="text-primary text-xs">컴포넌트 예시</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -257,7 +257,7 @@ export const GridPanel: React.FC<GridPanelProps> = ({
|
||||||
|
|
||||||
{/* 푸터 */}
|
{/* 푸터 */}
|
||||||
<div className="border-t border-gray-200 bg-gray-50 p-3">
|
<div className="border-t border-gray-200 bg-gray-50 p-3">
|
||||||
<div className="text-xs text-muted-foreground">💡 격자 설정은 실시간으로 캔버스에 반영됩니다 </div>
|
<div className="text-muted-foreground text-xs">💡 격자 설정은 실시간으로 캔버스에 반영됩니다 </div>
|
||||||
|
|
||||||
{/* 해상도 및 격자 정보 */}
|
{/* 해상도 및 격자 정보 */}
|
||||||
{screenResolution && actualGridInfo && (
|
{screenResolution && actualGridInfo && (
|
||||||
|
|
|
||||||
|
|
@ -481,9 +481,13 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
||||||
if (!selectedComponent) {
|
if (!selectedComponent) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col items-center justify-center p-6 text-center">
|
<div className="flex h-full flex-col items-center justify-center p-6 text-center">
|
||||||
<Settings className="mb-4 h-12 w-12 text-gray-400" />
|
<Settings className="text-muted-foreground mb-3 h-10 w-10" />
|
||||||
<h3 className="mb-2 text-lg font-medium text-gray-900">컴포넌트를 선택하세요</h3>
|
<h3 className="mb-2 text-sm font-semibold">컴포넌트를 선택하세요</h3>
|
||||||
<p className="text-sm text-gray-500">캔버스에서 컴포넌트를 클릭하면 속성을 편집할 수 있습니다.</p>
|
<p className="text-muted-foreground text-xs">
|
||||||
|
캔버스에서 컴포넌트를 클릭하면
|
||||||
|
<br />
|
||||||
|
속성을 편집할 수 있습니다.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -535,58 +539,58 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col">
|
||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
<div className="border-b border-gray-200 p-4">
|
<div className="border-b border-gray-200 p-4">
|
||||||
<div className="mb-3 flex items-center justify-between">
|
<div className="mb-4 flex items-center justify-between">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center gap-2">
|
||||||
<Settings className="text-muted-foreground h-4 w-4" />
|
<Settings className="text-muted-foreground h-4 w-4" />
|
||||||
<h3 className="font-medium text-gray-900">속성 편집</h3>
|
<h3 className="text-sm font-semibold">속성 편집</h3>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant="secondary" className="text-xs">
|
<Badge variant="secondary" className="text-xs font-medium">
|
||||||
{selectedComponent.type}
|
{selectedComponent.type}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 액션 버튼들 */}
|
{/* 액션 버튼들 */}
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-1.5">
|
||||||
<Button size="sm" variant="outline" onClick={onCopyComponent} className="flex items-center space-x-1">
|
<Button size="sm" variant="outline" onClick={onCopyComponent} className="h-8 px-2.5 text-xs">
|
||||||
<Copy className="h-3 w-3" />
|
<Copy className="mr-1 h-3 w-3" />
|
||||||
<span>복사</span>
|
복사
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{canGroup && (
|
{canGroup && (
|
||||||
<Button size="sm" variant="outline" onClick={onGroupComponents} className="flex items-center space-x-1">
|
<Button size="sm" variant="outline" onClick={onGroupComponents} className="h-8 px-2.5 text-xs">
|
||||||
<Group className="h-3 w-3" />
|
<Group className="mr-1 h-3 w-3" />
|
||||||
<span>그룹</span>
|
그룹
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{canUngroup && (
|
{canUngroup && (
|
||||||
<Button size="sm" variant="outline" onClick={onUngroupComponents} className="flex items-center space-x-1">
|
<Button size="sm" variant="outline" onClick={onUngroupComponents} className="h-8 px-2.5 text-xs">
|
||||||
<Ungroup className="h-3 w-3" />
|
<Ungroup className="mr-1 h-3 w-3" />
|
||||||
<span>해제</span>
|
해제
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button size="sm" variant="destructive" onClick={onDeleteComponent} className="flex items-center space-x-1">
|
<Button size="sm" variant="destructive" onClick={onDeleteComponent} className="h-8 px-2.5 text-xs">
|
||||||
<Trash2 className="h-3 w-3" />
|
<Trash2 className="mr-1 h-3 w-3" />
|
||||||
<span>삭제</span>
|
삭제
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 속성 편집 영역 */}
|
{/* 속성 편집 영역 */}
|
||||||
<div className="flex-1 space-y-6 overflow-y-auto p-4">
|
<div className="flex-1 space-y-4 overflow-y-auto p-4">
|
||||||
{/* 기본 정보 */}
|
{/* 기본 정보 */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center gap-2">
|
||||||
<Type className="text-muted-foreground h-4 w-4" />
|
<Type className="text-muted-foreground h-4 w-4" />
|
||||||
<h4 className="font-medium text-gray-900">기본 정보</h4>
|
<h4 className="text-sm font-semibold">기본 정보</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{selectedComponent.type === "widget" && (
|
{selectedComponent.type === "widget" && (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div className="space-y-1.5">
|
||||||
<Label htmlFor="columnName" className="text-sm font-medium">
|
<Label htmlFor="columnName" className="text-xs font-medium">
|
||||||
컬럼명 (읽기 전용)
|
컬럼명 (읽기 전용)
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -594,21 +598,20 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
||||||
value={selectedComponent.columnName || ""}
|
value={selectedComponent.columnName || ""}
|
||||||
readOnly
|
readOnly
|
||||||
placeholder="데이터베이스 컬럼명"
|
placeholder="데이터베이스 컬럼명"
|
||||||
className="text-muted-foreground mt-1 bg-gray-50"
|
className="bg-muted/50 text-muted-foreground h-8"
|
||||||
title="컬럼명은 변경할 수 없습니다"
|
title="컬럼명은 변경할 수 없습니다"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div className="space-y-1.5">
|
||||||
<Label htmlFor="inputType" className="text-sm font-medium">
|
<Label htmlFor="inputType" className="text-xs font-medium">
|
||||||
입력 타입
|
입력 타입
|
||||||
</Label>
|
</Label>
|
||||||
<select
|
<select
|
||||||
className="focus:border-primary mt-1 w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
className="border-input bg-background focus-visible:ring-ring flex h-8 w-full rounded-md border px-3 py-1 text-xs shadow-sm transition-colors focus-visible:ring-1 focus-visible:outline-none"
|
||||||
value={getBaseInputType(localInputs.widgetType)}
|
value={getBaseInputType(localInputs.widgetType)}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const selectedInputType = e.target.value as BaseInputType;
|
const selectedInputType = e.target.value as BaseInputType;
|
||||||
// 입력 타입에 맞는 기본 세부 타입 설정
|
|
||||||
const defaultWebType = getDefaultDetailType(selectedInputType);
|
const defaultWebType = getDefaultDetailType(selectedInputType);
|
||||||
setLocalInputs((prev) => ({ ...prev, widgetType: defaultWebType }));
|
setLocalInputs((prev) => ({ ...prev, widgetType: defaultWebType }));
|
||||||
onUpdateProperty("widgetType", defaultWebType);
|
onUpdateProperty("widgetType", defaultWebType);
|
||||||
|
|
@ -620,11 +623,11 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
<p className="mt-1 text-xs text-gray-500">세부 타입은 "상세 설정" 패널에서 선택하세요</p>
|
<p className="text-muted-foreground text-xs">세부 타입은 "상세 설정" 패널에서 선택하세요</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div className="space-y-1.5">
|
||||||
<Label htmlFor="placeholder" className="text-sm font-medium">
|
<Label htmlFor="placeholder" className="text-xs font-medium">
|
||||||
플레이스홀더
|
플레이스홀더
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -632,12 +635,11 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
||||||
value={localInputs.placeholder}
|
value={localInputs.placeholder}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const newValue = e.target.value;
|
const newValue = e.target.value;
|
||||||
// console.log("🔄 placeholder 변경:", newValue);
|
|
||||||
setLocalInputs((prev) => ({ ...prev, placeholder: newValue }));
|
setLocalInputs((prev) => ({ ...prev, placeholder: newValue }));
|
||||||
onUpdateProperty("placeholder", newValue);
|
onUpdateProperty("placeholder", newValue);
|
||||||
}}
|
}}
|
||||||
placeholder="입력 힌트 텍스트"
|
placeholder="입력 힌트 텍스트"
|
||||||
className="mt-1"
|
className="h-8"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ const getWidgetIcon = (widgetType: WebType) => {
|
||||||
case "text":
|
case "text":
|
||||||
case "email":
|
case "email":
|
||||||
case "tel":
|
case "tel":
|
||||||
return <Type className="h-3 w-3 text-primary" />;
|
return <Type className="text-primary h-3 w-3" />;
|
||||||
case "number":
|
case "number":
|
||||||
case "decimal":
|
case "decimal":
|
||||||
return <Hash className="h-3 w-3 text-green-600" />;
|
return <Hash className="h-3 w-3 text-green-600" />;
|
||||||
|
|
@ -49,9 +49,9 @@ const getWidgetIcon = (widgetType: WebType) => {
|
||||||
return <AlignLeft className="h-3 w-3 text-indigo-600" />;
|
return <AlignLeft className="h-3 w-3 text-indigo-600" />;
|
||||||
case "boolean":
|
case "boolean":
|
||||||
case "checkbox":
|
case "checkbox":
|
||||||
return <CheckSquare className="h-3 w-3 text-primary" />;
|
return <CheckSquare className="text-primary h-3 w-3" />;
|
||||||
case "code":
|
case "code":
|
||||||
return <Code className="h-3 w-3 text-muted-foreground" />;
|
return <Code className="text-muted-foreground h-3 w-3" />;
|
||||||
case "entity":
|
case "entity":
|
||||||
return <Building className="h-3 w-3 text-cyan-600" />;
|
return <Building className="h-3 w-3 text-cyan-600" />;
|
||||||
case "file":
|
case "file":
|
||||||
|
|
@ -89,55 +89,55 @@ export const TablesPanel: React.FC<TablesPanelProps> = ({
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col">
|
||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
<div className="border-b border-gray-200 p-4">
|
<div className="border-b p-4">
|
||||||
{selectedTableName && (
|
{selectedTableName && (
|
||||||
<div className="mb-3 rounded-md bg-accent p-3">
|
<div className="border-primary/20 bg-primary/5 mb-3 rounded-lg border p-3">
|
||||||
<div className="text-sm font-medium text-blue-900">선택된 테이블</div>
|
<div className="text-xs font-semibold">선택된 테이블</div>
|
||||||
<div className="mt-1 flex items-center space-x-2">
|
<div className="mt-1.5 flex items-center gap-2">
|
||||||
<Database className="h-3 w-3 text-primary" />
|
<Database className="text-primary h-3 w-3" />
|
||||||
<span className="font-mono text-xs text-blue-800">{selectedTableName}</span>
|
<span className="font-mono text-xs font-medium">{selectedTableName}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 검색 */}
|
{/* 검색 */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 transform text-gray-400" />
|
<Search className="text-muted-foreground absolute top-1/2 left-2.5 h-3.5 w-3.5 -translate-y-1/2" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="테이블명, 컬럼명으로 검색..."
|
placeholder="테이블명, 컬럼명 검색..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => onSearchChange(e.target.value)}
|
onChange={(e) => onSearchChange(e.target.value)}
|
||||||
className="w-full rounded-md border border-gray-300 py-2 pr-3 pl-10 focus:border-transparent focus:ring-2 focus:ring-blue-500"
|
className="border-input bg-background focus-visible:ring-ring h-8 w-full rounded-md border px-3 pl-8 text-xs focus-visible:ring-1 focus-visible:outline-none"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-2 text-xs text-muted-foreground">총 {filteredTables.length}개 테이블</div>
|
<div className="text-muted-foreground mt-2 text-xs">총 {filteredTables.length}개</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 테이블 목록 */}
|
{/* 테이블 목록 */}
|
||||||
<div className="scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-gray-100 flex-1 overflow-y-auto">
|
<div className="flex-1 overflow-y-auto">
|
||||||
<div className="space-y-1 p-2">
|
<div className="space-y-1.5 p-3">
|
||||||
{filteredTables.map((table) => {
|
{filteredTables.map((table) => {
|
||||||
const isExpanded = expandedTables.has(table.tableName);
|
const isExpanded = expandedTables.has(table.tableName);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={table.tableName} className="rounded-md border border-gray-200">
|
<div key={table.tableName} className="bg-card rounded-lg border">
|
||||||
{/* 테이블 헤더 */}
|
{/* 테이블 헤더 */}
|
||||||
<div
|
<div
|
||||||
className="flex cursor-pointer items-center justify-between p-3 hover:bg-gray-50"
|
className="hover:bg-accent/50 flex cursor-pointer items-center justify-between p-2.5 transition-colors"
|
||||||
onClick={() => toggleTable(table.tableName)}
|
onClick={() => toggleTable(table.tableName)}
|
||||||
>
|
>
|
||||||
<div className="flex flex-1 items-center space-x-2">
|
<div className="flex flex-1 items-center gap-2">
|
||||||
{isExpanded ? (
|
{isExpanded ? (
|
||||||
<ChevronDown className="h-4 w-4 text-gray-500" />
|
<ChevronDown className="text-muted-foreground h-3.5 w-3.5" />
|
||||||
) : (
|
) : (
|
||||||
<ChevronRight className="h-4 w-4 text-gray-500" />
|
<ChevronRight className="text-muted-foreground h-3.5 w-3.5" />
|
||||||
)}
|
)}
|
||||||
<Database className="h-4 w-4 text-primary" />
|
<Database className="text-primary h-3.5 w-3.5" />
|
||||||
<div className="flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="text-sm font-medium">{table.tableLabel || table.tableName}</div>
|
<div className="truncate text-xs font-semibold">{table.tableLabel || table.tableName}</div>
|
||||||
<div className="text-xs text-gray-500">{table.columns.length}개 컬럼</div>
|
<div className="text-muted-foreground text-xs">{table.columns.length}개</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -146,7 +146,7 @@ export const TablesPanel: React.FC<TablesPanelProps> = ({
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
draggable
|
draggable
|
||||||
onDragStart={(e) => onDragStart(e, table)}
|
onDragStart={(e) => onDragStart(e, table)}
|
||||||
className="ml-2 text-xs"
|
className="h-6 px-2 text-xs"
|
||||||
>
|
>
|
||||||
드래그
|
드래그
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -154,43 +154,33 @@ export const TablesPanel: React.FC<TablesPanelProps> = ({
|
||||||
|
|
||||||
{/* 컬럼 목록 */}
|
{/* 컬럼 목록 */}
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<div className="border-t border-gray-200 bg-gray-50">
|
<div className="bg-muted/30 border-t">
|
||||||
<div
|
<div className={`${table.columns.length > 8 ? "max-h-64 overflow-y-auto" : ""}`}>
|
||||||
className={`${
|
|
||||||
table.columns.length > 8
|
|
||||||
? "scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-gray-100 max-h-64 overflow-y-auto"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
style={{
|
|
||||||
scrollbarWidth: "thin",
|
|
||||||
scrollbarColor: "#cbd5e1 #f1f5f9",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{table.columns.map((column, index) => (
|
{table.columns.map((column, index) => (
|
||||||
<div
|
<div
|
||||||
key={column.columnName}
|
key={column.columnName}
|
||||||
className={`flex cursor-pointer items-center justify-between p-2 hover:bg-white ${
|
className={`hover:bg-accent/50 flex cursor-grab items-center justify-between p-2 transition-colors ${
|
||||||
index < table.columns.length - 1 ? "border-b border-gray-100" : ""
|
index < table.columns.length - 1 ? "border-border/50 border-b" : ""
|
||||||
}`}
|
}`}
|
||||||
draggable
|
draggable
|
||||||
onDragStart={(e) => onDragStart(e, table, column)}
|
onDragStart={(e) => onDragStart(e, table, column)}
|
||||||
>
|
>
|
||||||
<div className="flex flex-1 items-center space-x-2">
|
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||||
{getWidgetIcon(column.widgetType)}
|
{getWidgetIcon(column.widgetType)}
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="truncate text-sm font-medium">
|
<div className="truncate text-xs font-semibold">
|
||||||
{column.columnLabel || column.columnName}
|
{column.columnLabel || column.columnName}
|
||||||
</div>
|
</div>
|
||||||
<div className="truncate text-xs text-gray-500">{column.dataType}</div>
|
<div className="text-muted-foreground truncate text-xs">{column.dataType}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-shrink-0 items-center space-x-1">
|
<div className="flex flex-shrink-0 items-center gap-1">
|
||||||
<Badge variant="secondary" className="text-xs">
|
<Badge variant="secondary" className="h-4 px-1.5 text-xs">
|
||||||
{column.widgetType}
|
{column.widgetType}
|
||||||
</Badge>
|
</Badge>
|
||||||
{column.required && (
|
{column.required && (
|
||||||
<Badge variant="destructive" className="text-xs">
|
<Badge variant="destructive" className="h-4 px-1.5 text-xs">
|
||||||
필수
|
필수
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
|
@ -200,8 +190,8 @@ export const TablesPanel: React.FC<TablesPanelProps> = ({
|
||||||
|
|
||||||
{/* 컬럼 수가 많을 때 안내 메시지 */}
|
{/* 컬럼 수가 많을 때 안내 메시지 */}
|
||||||
{table.columns.length > 8 && (
|
{table.columns.length > 8 && (
|
||||||
<div className="sticky bottom-0 bg-gray-100 p-2 text-center">
|
<div className="bg-muted sticky bottom-0 p-2 text-center">
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-muted-foreground text-xs">
|
||||||
📜 총 {table.columns.length}개 컬럼 (스크롤하여 더 보기)
|
📜 총 {table.columns.length}개 컬럼 (스크롤하여 더 보기)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -217,7 +207,7 @@ export const TablesPanel: React.FC<TablesPanelProps> = ({
|
||||||
|
|
||||||
{/* 푸터 */}
|
{/* 푸터 */}
|
||||||
<div className="border-t border-gray-200 bg-gray-50 p-3">
|
<div className="border-t border-gray-200 bg-gray-50 p-3">
|
||||||
<div className="text-xs text-muted-foreground">💡 테이블이나 컬럼을 캔버스로 드래그하세요</div>
|
<div className="text-muted-foreground text-xs">💡 테이블이나 컬럼을 캔버스로 드래그하세요</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,11 @@ export default function InputWidget({ widget, value, onChange, className }: Inpu
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("space-y-2", className)}>
|
<div className={cn("space-y-1.5", className)}>
|
||||||
{widget.label && (
|
{widget.label && (
|
||||||
<Label htmlFor={widget.id} className="text-sm font-medium">
|
<Label htmlFor={widget.id} className="text-xs font-medium">
|
||||||
{widget.label}
|
{widget.label}
|
||||||
{widget.required && <span className="ml-1 text-red-500">*</span>}
|
{widget.required && <span className="text-destructive ml-0.5">*</span>}
|
||||||
</Label>
|
</Label>
|
||||||
)}
|
)}
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -33,7 +33,7 @@ export default function InputWidget({ widget, value, onChange, className }: Inpu
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
required={widget.required}
|
required={widget.required}
|
||||||
readOnly={widget.readonly}
|
readOnly={widget.readonly}
|
||||||
className={cn("w-full", widget.readonly && "cursor-not-allowed bg-gray-50")}
|
className={cn("h-9 w-full text-sm", widget.readonly && "bg-muted/50 cursor-not-allowed")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -45,20 +45,20 @@ export default function SelectWidget({ widget, value, onChange, options = [], cl
|
||||||
const displayOptions = options.length > 0 ? options : getDefaultOptions();
|
const displayOptions = options.length > 0 ? options : getDefaultOptions();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("space-y-2", className)}>
|
<div className={cn("space-y-1.5", className)}>
|
||||||
{widget.label && (
|
{widget.label && (
|
||||||
<Label htmlFor={widget.id} className="text-sm font-medium">
|
<Label htmlFor={widget.id} className="text-xs font-medium">
|
||||||
{widget.label}
|
{widget.label}
|
||||||
{widget.required && <span className="ml-1 text-red-500">*</span>}
|
{widget.required && <span className="text-destructive ml-0.5">*</span>}
|
||||||
</Label>
|
</Label>
|
||||||
)}
|
)}
|
||||||
<Select value={value} onValueChange={handleChange} disabled={widget.readonly}>
|
<Select value={value} onValueChange={handleChange} disabled={widget.readonly}>
|
||||||
<SelectTrigger className="w-full">
|
<SelectTrigger className="h-9 w-full text-sm">
|
||||||
<SelectValue placeholder={widget.placeholder || "선택해주세요"} />
|
<SelectValue placeholder={widget.placeholder || "선택해주세요"} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{displayOptions.map((option) => (
|
{displayOptions.map((option) => (
|
||||||
<SelectItem key={option.value} value={option.value}>
|
<SelectItem key={option.value} value={option.value} className="text-sm">
|
||||||
{option.label}
|
{option.label}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,11 @@ export default function TextareaWidget({ widget, value, onChange, className }: T
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("space-y-2", className)}>
|
<div className={cn("space-y-1.5", className)}>
|
||||||
{widget.label && (
|
{widget.label && (
|
||||||
<Label htmlFor={widget.id} className="text-sm font-medium">
|
<Label htmlFor={widget.id} className="text-xs font-medium">
|
||||||
{widget.label}
|
{widget.label}
|
||||||
{widget.required && <span className="ml-1 text-red-500">*</span>}
|
{widget.required && <span className="text-destructive ml-0.5">*</span>}
|
||||||
</Label>
|
</Label>
|
||||||
)}
|
)}
|
||||||
<Textarea
|
<Textarea
|
||||||
|
|
@ -32,7 +32,7 @@ export default function TextareaWidget({ widget, value, onChange, className }: T
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
required={widget.required}
|
required={widget.required}
|
||||||
readOnly={widget.readonly}
|
readOnly={widget.readonly}
|
||||||
className={cn("min-h-[100px] w-full", widget.readonly && "cursor-not-allowed bg-gray-50")}
|
className={cn("min-h-[80px] w-full text-sm", widget.readonly && "bg-muted/50 cursor-not-allowed")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -107,11 +107,13 @@ export const DynamicWebTypeRenderer: React.FC<DynamicComponentProps> = ({
|
||||||
if (!webTypeDefinition.isActive) {
|
if (!webTypeDefinition.isActive) {
|
||||||
console.warn(`웹타입 "${webType}"이 비활성화되어 있습니다.`);
|
console.warn(`웹타입 "${webType}"이 비활성화되어 있습니다.`);
|
||||||
return (
|
return (
|
||||||
<div className="rounded-md border border-dashed border-yellow-300 bg-yellow-50 p-4">
|
<div className="rounded-md border border-dashed border-yellow-500/30 bg-yellow-50 p-3 dark:bg-yellow-950/20">
|
||||||
<div className="flex items-center gap-2 text-yellow-600">
|
<div className="flex items-center gap-2 text-yellow-700 dark:text-yellow-400">
|
||||||
<span className="text-sm font-medium">⚠️ 비활성화된 웹타입</span>
|
<span className="text-xs font-medium">⚠️ 비활성화된 웹타입</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1 text-xs text-yellow-500">웹타입 "{webType}"이 비활성화되어 있습니다.</p>
|
<p className="mt-1 text-xs text-yellow-600 dark:text-yellow-500">
|
||||||
|
웹타입 "{webType}"이 비활성화되어 있습니다.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -163,11 +165,11 @@ export const DynamicWebTypeRenderer: React.FC<DynamicComponentProps> = ({
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`웹타입 "${webType}" 폴백 컴포넌트 렌더링 실패:`, error);
|
console.error(`웹타입 "${webType}" 폴백 컴포넌트 렌더링 실패:`, error);
|
||||||
return (
|
return (
|
||||||
<div className="rounded-md border border-dashed border-red-300 bg-red-50 p-4">
|
<div className="border-destructive/30 bg-destructive/5 rounded-md border border-dashed p-3">
|
||||||
<div className="flex items-center gap-2 text-red-600">
|
<div className="text-destructive flex items-center gap-2">
|
||||||
<span className="text-sm font-medium">⚠️ 알 수 없는 웹타입</span>
|
<span className="text-xs font-medium">⚠️ 알 수 없는 웹타입</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1 text-xs text-red-500">웹타입 "{webType}"을 렌더링할 수 없습니다.</p>
|
<p className="text-destructive/80 mt-1 text-xs">웹타입 "{webType}"을 렌더링할 수 없습니다.</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -188,8 +190,8 @@ export const WebTypePreviewRenderer: React.FC<{
|
||||||
|
|
||||||
if (!webTypeDefinition) {
|
if (!webTypeDefinition) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded border border-dashed border-gray-300 bg-gray-50 p-2 text-center">
|
<div className="border-border bg-muted/30 rounded-md border border-dashed p-2 text-center">
|
||||||
<span className="text-xs text-gray-500">웹타입 없음</span>
|
<span className="text-muted-foreground text-xs">웹타입 없음</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -479,18 +479,19 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
<button
|
<button
|
||||||
type={componentConfig.actionType || "button"}
|
type={componentConfig.actionType || "button"}
|
||||||
disabled={componentConfig.disabled || false}
|
disabled={componentConfig.disabled || false}
|
||||||
|
className="transition-all duration-200"
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
minHeight: "100%",
|
minHeight: "100%",
|
||||||
maxHeight: "100%",
|
maxHeight: "100%",
|
||||||
border: "none",
|
border: "none",
|
||||||
borderRadius: "8px",
|
borderRadius: "0.5rem",
|
||||||
background: componentConfig.disabled
|
background: componentConfig.disabled
|
||||||
? "linear-gradient(135deg, #e5e7eb 0%, #d1d5db 100%)"
|
? "linear-gradient(135deg, #e5e7eb 0%, #d1d5db 100%)"
|
||||||
: `linear-gradient(135deg, ${buttonColor} 0%, ${buttonDarkColor} 100%)`,
|
: `linear-gradient(135deg, ${buttonColor} 0%, ${buttonDarkColor} 100%)`,
|
||||||
color: componentConfig.disabled ? "#9ca3af" : "white",
|
color: componentConfig.disabled ? "#9ca3af" : "white",
|
||||||
fontSize: "14px",
|
fontSize: "0.875rem",
|
||||||
fontWeight: "600",
|
fontWeight: "600",
|
||||||
cursor: componentConfig.disabled ? "not-allowed" : "pointer",
|
cursor: componentConfig.disabled ? "not-allowed" : "pointer",
|
||||||
outline: "none",
|
outline: "none",
|
||||||
|
|
@ -498,11 +499,11 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
padding: "0 16px",
|
padding: "0 1rem",
|
||||||
margin: "0",
|
margin: "0",
|
||||||
lineHeight: "1",
|
lineHeight: "1.25",
|
||||||
minHeight: "36px",
|
minHeight: "2.25rem",
|
||||||
boxShadow: componentConfig.disabled ? "0 1px 2px 0 rgba(0, 0, 0, 0.05)" : `0 2px 4px 0 ${buttonColor}33`, // 33은 20% 투명도
|
boxShadow: componentConfig.disabled ? "0 1px 2px 0 rgba(0, 0, 0, 0.05)" : `0 1px 3px 0 ${buttonColor}40`,
|
||||||
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
...(isInteractive && component.style ? component.style : {}),
|
...(isInteractive && component.style ? component.style : {}),
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -135,9 +135,9 @@ export const CheckboxBasicComponent: React.FC<CheckboxBasicComponentProps> = ({
|
||||||
checked={checkedValues.includes(option.value)}
|
checked={checkedValues.includes(option.value)}
|
||||||
onChange={(e) => handleGroupChange(option.value, e.target.checked)}
|
onChange={(e) => handleGroupChange(option.value, e.target.checked)}
|
||||||
disabled={componentConfig.disabled || isDesignMode}
|
disabled={componentConfig.disabled || isDesignMode}
|
||||||
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-0 focus:outline-none"
|
className="border-input text-primary h-4 w-4 rounded focus:ring-0 focus:outline-none"
|
||||||
/>
|
/>
|
||||||
<span className="text-sm text-gray-900">{option.label}</span>
|
<span className="text-sm">{option.label}</span>
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -153,9 +153,9 @@ export const CheckboxBasicComponent: React.FC<CheckboxBasicComponentProps> = ({
|
||||||
disabled={componentConfig.disabled || isDesignMode}
|
disabled={componentConfig.disabled || isDesignMode}
|
||||||
required={componentConfig.required || false}
|
required={componentConfig.required || false}
|
||||||
onChange={(e) => handleCheckboxChange(e.target.checked)}
|
onChange={(e) => handleCheckboxChange(e.target.checked)}
|
||||||
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
className="border-input text-primary focus:ring-ring h-4 w-4 rounded"
|
||||||
/>
|
/>
|
||||||
<span className="text-sm text-gray-900">{componentConfig.checkboxLabel || component.text || "체크박스"}</span>
|
<span className="text-sm">{componentConfig.checkboxLabel || component.text || "체크박스"}</span>
|
||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -5,29 +5,29 @@
|
||||||
export const INPUT_CLASSES = {
|
export const INPUT_CLASSES = {
|
||||||
// 기본 input 스타일
|
// 기본 input 스타일
|
||||||
base: `
|
base: `
|
||||||
w-full h-full px-3 py-2 text-sm
|
w-full h-9 px-3 py-2 text-sm
|
||||||
border border-gray-300 rounded-md
|
border border-input rounded-md
|
||||||
bg-white text-gray-900
|
bg-background text-foreground
|
||||||
outline-none transition-all duration-200
|
transition-colors
|
||||||
focus:border-orange-500 focus:ring-2 focus:ring-orange-100
|
focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring
|
||||||
disabled:bg-gray-100 disabled:text-gray-400 disabled:cursor-not-allowed
|
disabled:cursor-not-allowed disabled:opacity-50
|
||||||
placeholder:text-gray-400
|
placeholder:text-muted-foreground
|
||||||
max-w-full box-border
|
max-w-full box-border
|
||||||
`,
|
`,
|
||||||
|
|
||||||
// 선택된 상태
|
// 선택된 상태
|
||||||
selected: `
|
selected: `
|
||||||
border-blue-500 ring-2 ring-blue-100
|
ring-2 ring-primary/20
|
||||||
`,
|
`,
|
||||||
|
|
||||||
// 라벨 스타일
|
// 라벨 스타일
|
||||||
label: `
|
label: `
|
||||||
absolute -top-6 left-0 text-sm font-medium text-slate-600
|
absolute -top-6 left-0 text-xs font-medium text-foreground
|
||||||
`,
|
`,
|
||||||
|
|
||||||
// 필수 표시
|
// 필수 표시
|
||||||
required: `
|
required: `
|
||||||
text-red-500
|
ml-0.5 text-destructive
|
||||||
`,
|
`,
|
||||||
|
|
||||||
// 컨테이너
|
// 컨테이너
|
||||||
|
|
@ -37,24 +37,24 @@ export const INPUT_CLASSES = {
|
||||||
|
|
||||||
// textarea
|
// textarea
|
||||||
textarea: `
|
textarea: `
|
||||||
w-full h-full px-3 py-2 text-sm
|
min-h-[80px] w-full px-3 py-2 text-sm
|
||||||
border border-gray-300 rounded-md
|
border border-input rounded-md
|
||||||
bg-white text-gray-900
|
bg-background text-foreground
|
||||||
outline-none transition-all duration-200
|
transition-colors
|
||||||
focus:border-orange-500 focus:ring-2 focus:ring-orange-100
|
focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring
|
||||||
disabled:bg-gray-100 disabled:text-gray-400 disabled:cursor-not-allowed
|
disabled:cursor-not-allowed disabled:opacity-50
|
||||||
resize-none
|
resize-none
|
||||||
max-w-full box-border
|
max-w-full box-border
|
||||||
`,
|
`,
|
||||||
|
|
||||||
// select
|
// select
|
||||||
select: `
|
select: `
|
||||||
w-full h-full px-3 py-2 text-sm
|
h-9 w-full px-3 py-2 text-sm
|
||||||
border border-gray-300 rounded-md
|
border border-input rounded-md
|
||||||
bg-white text-gray-900
|
bg-background text-foreground
|
||||||
outline-none transition-all duration-200
|
transition-colors
|
||||||
focus:border-orange-500 focus:ring-2 focus:ring-orange-100
|
focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring
|
||||||
disabled:bg-gray-100 disabled:text-gray-400 disabled:cursor-not-allowed
|
disabled:cursor-not-allowed disabled:opacity-50
|
||||||
cursor-pointer
|
cursor-pointer
|
||||||
max-w-full box-border
|
max-w-full box-border
|
||||||
`,
|
`,
|
||||||
|
|
@ -66,37 +66,37 @@ export const INPUT_CLASSES = {
|
||||||
|
|
||||||
// 구분자 (@ , ~ 등)
|
// 구분자 (@ , ~ 등)
|
||||||
separator: `
|
separator: `
|
||||||
text-base font-medium text-gray-500
|
text-sm font-medium text-muted-foreground
|
||||||
`,
|
`,
|
||||||
|
|
||||||
// Currency 통화 기호
|
// Currency 통화 기호
|
||||||
currencySymbol: `
|
currencySymbol: `
|
||||||
text-base font-semibold text-green-600 pl-2
|
text-sm font-semibold text-green-600 pl-2
|
||||||
`,
|
`,
|
||||||
|
|
||||||
// Currency input
|
// Currency input
|
||||||
currencyInput: `
|
currencyInput: `
|
||||||
flex-1 h-full px-3 py-2 text-base font-semibold text-right
|
flex-1 h-9 px-3 py-2 text-sm font-semibold text-right
|
||||||
border border-gray-300 rounded-md
|
border border-input rounded-md
|
||||||
bg-white text-green-600
|
bg-background text-green-600
|
||||||
outline-none transition-all duration-200
|
transition-colors
|
||||||
focus:border-orange-500 focus:ring-2 focus:ring-orange-100
|
focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring
|
||||||
disabled:bg-gray-100 disabled:text-gray-400
|
disabled:cursor-not-allowed disabled:opacity-50
|
||||||
`,
|
`,
|
||||||
|
|
||||||
// Percentage 퍼센트 기호
|
// Percentage 퍼센트 기호
|
||||||
percentageSymbol: `
|
percentageSymbol: `
|
||||||
text-base font-semibold text-blue-600 pr-2
|
text-sm font-semibold text-blue-600 pr-2
|
||||||
`,
|
`,
|
||||||
|
|
||||||
// Percentage input
|
// Percentage input
|
||||||
percentageInput: `
|
percentageInput: `
|
||||||
flex-1 h-full px-3 py-2 text-base font-semibold text-right
|
flex-1 h-9 px-3 py-2 text-sm font-semibold text-right
|
||||||
border border-gray-300 rounded-md
|
border border-input rounded-md
|
||||||
bg-white text-blue-600
|
bg-background text-blue-600
|
||||||
outline-none transition-all duration-200
|
transition-colors
|
||||||
focus:border-orange-500 focus:ring-2 focus:ring-orange-100
|
focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring
|
||||||
disabled:bg-gray-100 disabled:text-gray-400
|
disabled:cursor-not-allowed disabled:opacity-50
|
||||||
`,
|
`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,34 +29,34 @@ import {
|
||||||
|
|
||||||
// 파일 아이콘 매핑
|
// 파일 아이콘 매핑
|
||||||
const getFileIcon = (extension: string) => {
|
const getFileIcon = (extension: string) => {
|
||||||
const ext = extension.toLowerCase().replace('.', '');
|
const ext = extension.toLowerCase().replace(".", "");
|
||||||
|
|
||||||
if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(ext)) {
|
if (["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"].includes(ext)) {
|
||||||
return <FileImage className="w-6 h-6 text-blue-500" />;
|
return <FileImage className="h-6 w-6 text-blue-500" />;
|
||||||
}
|
}
|
||||||
if (['mp4', 'avi', 'mov', 'wmv', 'flv', 'webm'].includes(ext)) {
|
if (["mp4", "avi", "mov", "wmv", "flv", "webm"].includes(ext)) {
|
||||||
return <FileVideo className="w-6 h-6 text-purple-500" />;
|
return <FileVideo className="h-6 w-6 text-purple-500" />;
|
||||||
}
|
}
|
||||||
if (['mp3', 'wav', 'flac', 'aac', 'ogg'].includes(ext)) {
|
if (["mp3", "wav", "flac", "aac", "ogg"].includes(ext)) {
|
||||||
return <FileAudio className="w-6 h-6 text-green-500" />;
|
return <FileAudio className="h-6 w-6 text-green-500" />;
|
||||||
}
|
}
|
||||||
if (['pdf'].includes(ext)) {
|
if (["pdf"].includes(ext)) {
|
||||||
return <FileText className="w-6 h-6 text-red-500" />;
|
return <FileText className="h-6 w-6 text-red-500" />;
|
||||||
}
|
}
|
||||||
if (['doc', 'docx', 'hwp', 'hwpx', 'pages'].includes(ext)) {
|
if (["doc", "docx", "hwp", "hwpx", "pages"].includes(ext)) {
|
||||||
return <FileText className="w-6 h-6 text-blue-600" />;
|
return <FileText className="h-6 w-6 text-blue-600" />;
|
||||||
}
|
}
|
||||||
if (['xls', 'xlsx', 'hcell', 'numbers'].includes(ext)) {
|
if (["xls", "xlsx", "hcell", "numbers"].includes(ext)) {
|
||||||
return <FileText className="w-6 h-6 text-green-600" />;
|
return <FileText className="h-6 w-6 text-green-600" />;
|
||||||
}
|
}
|
||||||
if (['ppt', 'pptx', 'hanshow', 'keynote'].includes(ext)) {
|
if (["ppt", "pptx", "hanshow", "keynote"].includes(ext)) {
|
||||||
return <Presentation className="w-6 h-6 text-orange-500" />;
|
return <Presentation className="h-6 w-6 text-orange-500" />;
|
||||||
}
|
}
|
||||||
if (['zip', 'rar', '7z', 'tar', 'gz'].includes(ext)) {
|
if (["zip", "rar", "7z", "tar", "gz"].includes(ext)) {
|
||||||
return <Archive className="w-6 h-6 text-gray-500" />;
|
return <Archive className="h-6 w-6 text-gray-500" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <File className="w-6 h-6 text-gray-400" />;
|
return <File className="h-6 w-6 text-gray-400" />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface FileUploadComponentProps {
|
export interface FileUploadComponentProps {
|
||||||
|
|
@ -92,7 +92,7 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
onUpdate,
|
onUpdate,
|
||||||
}) => {
|
}) => {
|
||||||
const [uploadedFiles, setUploadedFiles] = useState<FileInfo[]>([]);
|
const [uploadedFiles, setUploadedFiles] = useState<FileInfo[]>([]);
|
||||||
const [uploadStatus, setUploadStatus] = useState<FileUploadStatus>('idle');
|
const [uploadStatus, setUploadStatus] = useState<FileUploadStatus>("idle");
|
||||||
const [dragOver, setDragOver] = useState(false);
|
const [dragOver, setDragOver] = useState(false);
|
||||||
const [viewerFile, setViewerFile] = useState<FileInfo | null>(null);
|
const [viewerFile, setViewerFile] = useState<FileInfo | null>(null);
|
||||||
const [isViewerOpen, setIsViewerOpen] = useState(false);
|
const [isViewerOpen, setIsViewerOpen] = useState(false);
|
||||||
|
|
@ -113,15 +113,15 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
console.log("🚀 컴포넌트 마운트 시 파일 즉시 복원:", {
|
console.log("🚀 컴포넌트 마운트 시 파일 즉시 복원:", {
|
||||||
componentId: component.id,
|
componentId: component.id,
|
||||||
restoredFiles: parsedFiles.length,
|
restoredFiles: parsedFiles.length,
|
||||||
files: parsedFiles.map((f: any) => ({ objid: f.objid, name: f.realFileName }))
|
files: parsedFiles.map((f: any) => ({ objid: f.objid, name: f.realFileName })),
|
||||||
});
|
});
|
||||||
setUploadedFiles(parsedFiles);
|
setUploadedFiles(parsedFiles);
|
||||||
|
|
||||||
// 전역 상태에도 복원
|
// 전역 상태에도 복원
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== "undefined") {
|
||||||
(window as any).globalFileState = {
|
(window as any).globalFileState = {
|
||||||
...(window as any).globalFileState,
|
...(window as any).globalFileState,
|
||||||
[component.id]: parsedFiles
|
[component.id]: parsedFiles,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -141,15 +141,15 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
filesCount: event.detail.files?.length || 0,
|
filesCount: event.detail.files?.length || 0,
|
||||||
action: event.detail.action,
|
action: event.detail.action,
|
||||||
source: event.detail.source,
|
source: event.detail.source,
|
||||||
eventDetail: event.detail
|
eventDetail: event.detail,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 현재 컴포넌트와 일치하고 화면설계 모드에서 온 이벤트인 경우
|
// 현재 컴포넌트와 일치하고 화면설계 모드에서 온 이벤트인 경우
|
||||||
if (event.detail.componentId === component.id && event.detail.source === 'designMode') {
|
if (event.detail.componentId === component.id && event.detail.source === "designMode") {
|
||||||
console.log("✅✅✅ 화면설계 모드 → 실제 화면 파일 동기화 시작:", {
|
console.log("✅✅✅ 화면설계 모드 → 실제 화면 파일 동기화 시작:", {
|
||||||
componentId: component.id,
|
componentId: component.id,
|
||||||
filesCount: event.detail.files?.length || 0,
|
filesCount: event.detail.files?.length || 0,
|
||||||
action: event.detail.action
|
action: event.detail.action,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 파일 상태 업데이트
|
// 파일 상태 업데이트
|
||||||
|
|
@ -162,17 +162,17 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
localStorage.setItem(backupKey, JSON.stringify(newFiles));
|
localStorage.setItem(backupKey, JSON.stringify(newFiles));
|
||||||
console.log("💾 화면설계 모드 동기화 후 localStorage 백업 업데이트:", {
|
console.log("💾 화면설계 모드 동기화 후 localStorage 백업 업데이트:", {
|
||||||
componentId: component.id,
|
componentId: component.id,
|
||||||
fileCount: newFiles.length
|
fileCount: newFiles.length,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("localStorage 백업 업데이트 실패:", e);
|
console.warn("localStorage 백업 업데이트 실패:", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 전역 상태 업데이트
|
// 전역 상태 업데이트
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== "undefined") {
|
||||||
(window as any).globalFileState = {
|
(window as any).globalFileState = {
|
||||||
...(window as any).globalFileState,
|
...(window as any).globalFileState,
|
||||||
[component.id]: newFiles
|
[component.id]: newFiles,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -180,22 +180,22 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
if (onUpdate) {
|
if (onUpdate) {
|
||||||
onUpdate({
|
onUpdate({
|
||||||
uploadedFiles: newFiles,
|
uploadedFiles: newFiles,
|
||||||
lastFileUpdate: event.detail.timestamp
|
lastFileUpdate: event.detail.timestamp,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("🎉🎉🎉 화면설계 모드 → 실제 화면 동기화 완료:", {
|
console.log("🎉🎉🎉 화면설계 모드 → 실제 화면 동기화 완료:", {
|
||||||
componentId: component.id,
|
componentId: component.id,
|
||||||
finalFileCount: newFiles.length
|
finalFileCount: newFiles.length,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== "undefined") {
|
||||||
window.addEventListener('globalFileStateChanged', handleDesignModeFileChange as EventListener);
|
window.addEventListener("globalFileStateChanged", handleDesignModeFileChange as EventListener);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('globalFileStateChanged', handleDesignModeFileChange as EventListener);
|
window.removeEventListener("globalFileStateChanged", handleDesignModeFileChange as EventListener);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [component.id, onUpdate]);
|
}, [component.id, onUpdate]);
|
||||||
|
|
@ -205,8 +205,10 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
if (!component?.id) return;
|
if (!component?.id) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let screenId = formData?.screenId || (typeof window !== 'undefined' && window.location.pathname.includes('/screens/')
|
let screenId =
|
||||||
? parseInt(window.location.pathname.split('/screens/')[1])
|
formData?.screenId ||
|
||||||
|
(typeof window !== "undefined" && window.location.pathname.includes("/screens/")
|
||||||
|
? parseInt(window.location.pathname.split("/screens/")[1])
|
||||||
: null);
|
: null);
|
||||||
|
|
||||||
// 디자인 모드인 경우 기본 화면 ID 사용
|
// 디자인 모드인 경우 기본 화면 ID 사용
|
||||||
|
|
@ -238,7 +240,7 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
dataFiles: response.dataFiles.length,
|
dataFiles: response.dataFiles.length,
|
||||||
totalFiles: response.totalFiles.length,
|
totalFiles: response.totalFiles.length,
|
||||||
summary: response.summary,
|
summary: response.summary,
|
||||||
actualFiles: response.totalFiles
|
actualFiles: response.totalFiles,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 파일 데이터 형식 통일
|
// 파일 데이터 형식 통일
|
||||||
|
|
@ -249,9 +251,9 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
fileSize: file.fileSize || file.file_size,
|
fileSize: file.fileSize || file.file_size,
|
||||||
fileExt: file.fileExt || file.file_ext,
|
fileExt: file.fileExt || file.file_ext,
|
||||||
regdate: file.regdate,
|
regdate: file.regdate,
|
||||||
status: file.status || 'ACTIVE',
|
status: file.status || "ACTIVE",
|
||||||
uploadedAt: file.uploadedAt || new Date().toISOString(),
|
uploadedAt: file.uploadedAt || new Date().toISOString(),
|
||||||
...file
|
...file,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
console.log("📁 형식 변환된 파일 데이터:", formattedFiles);
|
console.log("📁 형식 변환된 파일 데이터:", formattedFiles);
|
||||||
|
|
@ -275,7 +277,7 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
로컬파일: parsedBackupFiles.length,
|
로컬파일: parsedBackupFiles.length,
|
||||||
추가파일: additionalFiles.length,
|
추가파일: additionalFiles.length,
|
||||||
최종파일: finalFiles.length,
|
최종파일: finalFiles.length,
|
||||||
최종파일목록: finalFiles.map((f: any) => ({ objid: f.objid, name: f.realFileName }))
|
최종파일목록: finalFiles.map((f: any) => ({ objid: f.objid, name: f.realFileName })),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -285,10 +287,10 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
setUploadedFiles(finalFiles);
|
setUploadedFiles(finalFiles);
|
||||||
|
|
||||||
// 전역 상태에도 저장
|
// 전역 상태에도 저장
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== "undefined") {
|
||||||
(window as any).globalFileState = {
|
(window as any).globalFileState = {
|
||||||
...(window as any).globalFileState,
|
...(window as any).globalFileState,
|
||||||
[component.id]: finalFiles
|
[component.id]: finalFiles,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 🌐 전역 파일 저장소에 등록 (페이지 간 공유용)
|
// 🌐 전역 파일 저장소에 등록 (페이지 간 공유용)
|
||||||
|
|
@ -325,11 +327,11 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
componentFiles: componentFiles.length,
|
componentFiles: componentFiles.length,
|
||||||
formData: formData,
|
formData: formData,
|
||||||
screenId: formData?.screenId,
|
screenId: formData?.screenId,
|
||||||
currentUploadedFiles: uploadedFiles.length
|
currentUploadedFiles: uploadedFiles.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 먼저 새로운 템플릿 파일 조회 시도
|
// 먼저 새로운 템플릿 파일 조회 시도
|
||||||
loadComponentFiles().then(useNewLogic => {
|
loadComponentFiles().then((useNewLogic) => {
|
||||||
if (useNewLogic) {
|
if (useNewLogic) {
|
||||||
console.log("✅ 새로운 템플릿 파일 로직 사용");
|
console.log("✅ 새로운 템플릿 파일 로직 사용");
|
||||||
return; // 새로운 로직이 성공했으면 기존 로직 스킵
|
return; // 새로운 로직이 성공했으면 기존 로직 스킵
|
||||||
|
|
@ -339,7 +341,7 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
console.log("📂 기존 파일 로직 사용");
|
console.log("📂 기존 파일 로직 사용");
|
||||||
|
|
||||||
// 전역 상태에서 최신 파일 정보 가져오기
|
// 전역 상태에서 최신 파일 정보 가져오기
|
||||||
const globalFileState = typeof window !== 'undefined' ? (window as any).globalFileState || {} : {};
|
const globalFileState = typeof window !== "undefined" ? (window as any).globalFileState || {} : {};
|
||||||
const globalFiles = globalFileState[component.id] || [];
|
const globalFiles = globalFileState[component.id] || [];
|
||||||
|
|
||||||
// 최신 파일 정보 사용 (전역 상태 > 컴포넌트 속성)
|
// 최신 파일 정보 사용 (전역 상태 > 컴포넌트 속성)
|
||||||
|
|
@ -351,7 +353,7 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
globalFiles: globalFiles.length,
|
globalFiles: globalFiles.length,
|
||||||
currentFiles: currentFiles.length,
|
currentFiles: currentFiles.length,
|
||||||
uploadedFiles: uploadedFiles.length,
|
uploadedFiles: uploadedFiles.length,
|
||||||
lastUpdate: lastUpdate
|
lastUpdate: lastUpdate,
|
||||||
});
|
});
|
||||||
|
|
||||||
// localStorage에서 백업 파일 복원 (새로고침 시 중요!)
|
// localStorage에서 백업 파일 복원 (새로고침 시 중요!)
|
||||||
|
|
@ -364,15 +366,15 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
console.log("🔄 localStorage에서 파일 복원:", {
|
console.log("🔄 localStorage에서 파일 복원:", {
|
||||||
componentId: component.id,
|
componentId: component.id,
|
||||||
restoredFiles: parsedFiles.length,
|
restoredFiles: parsedFiles.length,
|
||||||
files: parsedFiles.map((f: any) => ({ objid: f.objid, name: f.realFileName }))
|
files: parsedFiles.map((f: any) => ({ objid: f.objid, name: f.realFileName })),
|
||||||
});
|
});
|
||||||
setUploadedFiles(parsedFiles);
|
setUploadedFiles(parsedFiles);
|
||||||
|
|
||||||
// 전역 상태에도 복원
|
// 전역 상태에도 복원
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== "undefined") {
|
||||||
(window as any).globalFileState = {
|
(window as any).globalFileState = {
|
||||||
...(window as any).globalFileState,
|
...(window as any).globalFileState,
|
||||||
[component.id]: parsedFiles
|
[component.id]: parsedFiles,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|
@ -388,10 +390,10 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
currentFiles: currentFiles.length,
|
currentFiles: currentFiles.length,
|
||||||
uploadedFiles: uploadedFiles.length,
|
uploadedFiles: uploadedFiles.length,
|
||||||
currentFilesData: currentFiles.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 }))
|
uploadedFilesData: uploadedFiles.map((f) => ({ objid: f.objid, name: f.realFileName })),
|
||||||
});
|
});
|
||||||
setUploadedFiles(currentFiles);
|
setUploadedFiles(currentFiles);
|
||||||
setForceUpdate(prev => prev + 1);
|
setForceUpdate((prev) => prev + 1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [loadComponentFiles, component.id, (component as any)?.uploadedFiles, (component as any)?.lastFileUpdate]);
|
}, [loadComponentFiles, component.id, (component as any)?.uploadedFiles, (component as any)?.lastFileUpdate]);
|
||||||
|
|
@ -408,7 +410,7 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
newFileCount: fileCount,
|
newFileCount: fileCount,
|
||||||
currentFileCount: uploadedFiles.length,
|
currentFileCount: uploadedFiles.length,
|
||||||
timestamp,
|
timestamp,
|
||||||
isRestore: !!isRestore
|
isRestore: !!isRestore,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 같은 컴포넌트 ID인 경우에만 업데이트
|
// 같은 컴포넌트 ID인 경우에만 업데이트
|
||||||
|
|
@ -418,11 +420,11 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
componentId: component.id,
|
componentId: component.id,
|
||||||
이전파일수: uploadedFiles?.length || 0,
|
이전파일수: uploadedFiles?.length || 0,
|
||||||
새파일수: files?.length || 0,
|
새파일수: files?.length || 0,
|
||||||
files: files?.map((f: any) => ({ objid: f.objid, name: f.realFileName })) || []
|
files: files?.map((f: any) => ({ objid: f.objid, name: f.realFileName })) || [],
|
||||||
});
|
});
|
||||||
|
|
||||||
setUploadedFiles(files);
|
setUploadedFiles(files);
|
||||||
setForceUpdate(prev => prev + 1);
|
setForceUpdate((prev) => prev + 1);
|
||||||
|
|
||||||
// localStorage 백업도 업데이트
|
// localStorage 백업도 업데이트
|
||||||
try {
|
try {
|
||||||
|
|
@ -434,11 +436,11 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== "undefined") {
|
||||||
window.addEventListener('globalFileStateChanged', handleGlobalFileStateChange as EventListener);
|
window.addEventListener("globalFileStateChanged", handleGlobalFileStateChange as EventListener);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('globalFileStateChanged', handleGlobalFileStateChange as EventListener);
|
window.removeEventListener("globalFileStateChanged", handleGlobalFileStateChange as EventListener);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [component.id, uploadedFiles.length]);
|
}, [component.id, uploadedFiles.length]);
|
||||||
|
|
@ -450,7 +452,7 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
multiple: safeComponentConfig.multiple || false,
|
multiple: safeComponentConfig.multiple || false,
|
||||||
maxSize: safeComponentConfig.maxSize || 10 * 1024 * 1024, // 10MB
|
maxSize: safeComponentConfig.maxSize || 10 * 1024 * 1024, // 10MB
|
||||||
maxFiles: safeComponentConfig.maxFiles || 5,
|
maxFiles: safeComponentConfig.maxFiles || 5,
|
||||||
...safeComponentConfig
|
...safeComponentConfig,
|
||||||
} as FileUploadConfig;
|
} as FileUploadConfig;
|
||||||
|
|
||||||
// 파일 선택 핸들러
|
// 파일 선택 핸들러
|
||||||
|
|
@ -459,7 +461,7 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
hasFileInputRef: !!fileInputRef.current,
|
hasFileInputRef: !!fileInputRef.current,
|
||||||
fileInputRef: fileInputRef.current,
|
fileInputRef: fileInputRef.current,
|
||||||
fileInputType: fileInputRef.current?.type,
|
fileInputType: fileInputRef.current?.type,
|
||||||
fileInputHidden: fileInputRef.current?.className
|
fileInputHidden: fileInputRef.current?.className,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (fileInputRef.current) {
|
if (fileInputRef.current) {
|
||||||
|
|
@ -478,21 +480,22 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 파일 업로드 처리
|
// 파일 업로드 처리
|
||||||
const handleFileUpload = useCallback(async (files: File[]) => {
|
const handleFileUpload = useCallback(
|
||||||
|
async (files: File[]) => {
|
||||||
if (!files.length) return;
|
if (!files.length) return;
|
||||||
|
|
||||||
// 중복 파일 체크
|
// 중복 파일 체크
|
||||||
const existingFileNames = uploadedFiles.map(f => f.realFileName.toLowerCase());
|
const existingFileNames = uploadedFiles.map((f) => f.realFileName.toLowerCase());
|
||||||
const duplicates: string[] = [];
|
const duplicates: string[] = [];
|
||||||
const uniqueFiles: File[] = [];
|
const uniqueFiles: File[] = [];
|
||||||
|
|
||||||
console.log("🔍 중복 파일 체크:", {
|
console.log("🔍 중복 파일 체크:", {
|
||||||
uploadedFiles: uploadedFiles.length,
|
uploadedFiles: uploadedFiles.length,
|
||||||
existingFileNames: existingFileNames,
|
existingFileNames: existingFileNames,
|
||||||
newFiles: files.map(f => f.name.toLowerCase())
|
newFiles: files.map((f) => f.name.toLowerCase()),
|
||||||
});
|
});
|
||||||
|
|
||||||
files.forEach(file => {
|
files.forEach((file) => {
|
||||||
const fileName = file.name.toLowerCase();
|
const fileName = file.name.toLowerCase();
|
||||||
if (existingFileNames.includes(fileName)) {
|
if (existingFileNames.includes(fileName)) {
|
||||||
duplicates.push(file.name);
|
duplicates.push(file.name);
|
||||||
|
|
@ -505,13 +508,13 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
|
|
||||||
console.log("🔍 중복 체크 결과:", {
|
console.log("🔍 중복 체크 결과:", {
|
||||||
duplicates: duplicates,
|
duplicates: duplicates,
|
||||||
uniqueFiles: uniqueFiles.map(f => f.name)
|
uniqueFiles: uniqueFiles.map((f) => f.name),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (duplicates.length > 0) {
|
if (duplicates.length > 0) {
|
||||||
toast.error(`중복된 파일이 있습니다: ${duplicates.join(', ')}`, {
|
toast.error(`중복된 파일이 있습니다: ${duplicates.join(", ")}`, {
|
||||||
description: "같은 이름의 파일이 이미 업로드되어 있습니다.",
|
description: "같은 이름의 파일이 이미 업로드되어 있습니다.",
|
||||||
duration: 4000
|
duration: 4000,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (uniqueFiles.length === 0) {
|
if (uniqueFiles.length === 0) {
|
||||||
|
|
@ -523,12 +526,12 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
const filesToUpload = uniqueFiles.length > 0 ? uniqueFiles : files;
|
const filesToUpload = uniqueFiles.length > 0 ? uniqueFiles : files;
|
||||||
setUploadStatus('uploading');
|
setUploadStatus("uploading");
|
||||||
toast.loading("파일을 업로드하는 중...", { id: 'file-upload' });
|
toast.loading("파일을 업로드하는 중...", { id: "file-upload" });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// targetObjid 생성 - 템플릿 vs 데이터 파일 구분
|
// targetObjid 생성 - 템플릿 vs 데이터 파일 구분
|
||||||
const tableName = formData?.tableName || component.tableName || 'default_table';
|
const tableName = formData?.tableName || component.tableName || "default_table";
|
||||||
const recordId = formData?.id;
|
const recordId = formData?.id;
|
||||||
const screenId = formData?.screenId;
|
const screenId = formData?.screenId;
|
||||||
const columnName = component.columnName || component.id;
|
const columnName = component.columnName || component.id;
|
||||||
|
|
@ -555,8 +558,8 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
recordId: formData?.recordId || recordId || `temp_${component.id}`,
|
recordId: formData?.recordId || recordId || `temp_${component.id}`,
|
||||||
columnName: formData?.columnName || columnName,
|
columnName: formData?.columnName || columnName,
|
||||||
isVirtualFileColumn: formData?.isVirtualFileColumn || true,
|
isVirtualFileColumn: formData?.isVirtualFileColumn || true,
|
||||||
docType: component.fileConfig?.docType || 'DOCUMENT',
|
docType: component.fileConfig?.docType || "DOCUMENT",
|
||||||
docTypeName: component.fileConfig?.docTypeName || '일반 문서',
|
docTypeName: component.fileConfig?.docTypeName || "일반 문서",
|
||||||
// 호환성을 위한 기존 필드들
|
// 호환성을 위한 기존 필드들
|
||||||
tableName: tableName,
|
tableName: tableName,
|
||||||
fieldName: columnName,
|
fieldName: columnName,
|
||||||
|
|
@ -566,13 +569,13 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
console.log("📤 파일 업로드 시작:", {
|
console.log("📤 파일 업로드 시작:", {
|
||||||
originalFiles: files.length,
|
originalFiles: files.length,
|
||||||
filesToUpload: filesToUpload.length,
|
filesToUpload: filesToUpload.length,
|
||||||
files: filesToUpload.map(f => ({ name: f.name, size: f.size })),
|
files: filesToUpload.map((f) => ({ name: f.name, size: f.size })),
|
||||||
uploadData
|
uploadData,
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await uploadFiles({
|
const response = await uploadFiles({
|
||||||
files: filesToUpload,
|
files: filesToUpload,
|
||||||
...uploadData
|
...uploadData,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("📤 파일 업로드 API 응답:", response);
|
console.log("📤 파일 업로드 API 응답:", response);
|
||||||
|
|
@ -585,7 +588,7 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
hasData: !!(response as any).data,
|
hasData: !!(response as any).data,
|
||||||
fileDataLength: fileData.length,
|
fileDataLength: fileData.length,
|
||||||
fileData: fileData,
|
fileData: fileData,
|
||||||
responseKeys: Object.keys(response)
|
responseKeys: Object.keys(response),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (fileData.length === 0) {
|
if (fileData.length === 0) {
|
||||||
|
|
@ -606,9 +609,9 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
companyCode: file.company_code || file.companyCode,
|
companyCode: file.company_code || file.companyCode,
|
||||||
writer: file.writer,
|
writer: file.writer,
|
||||||
regdate: file.regdate,
|
regdate: file.regdate,
|
||||||
status: file.status || 'ACTIVE',
|
status: file.status || "ACTIVE",
|
||||||
uploadedAt: new Date().toISOString(),
|
uploadedAt: new Date().toISOString(),
|
||||||
...file
|
...file,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
console.log("📁 변환된 파일 데이터:", newFiles);
|
console.log("📁 변환된 파일 데이터:", newFiles);
|
||||||
|
|
@ -618,11 +621,11 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
이전파일수: uploadedFiles.length,
|
이전파일수: uploadedFiles.length,
|
||||||
새파일수: newFiles.length,
|
새파일수: newFiles.length,
|
||||||
총파일수: updatedFiles.length,
|
총파일수: updatedFiles.length,
|
||||||
updatedFiles: updatedFiles.map(f => ({ objid: f.objid, name: f.realFileName }))
|
updatedFiles: updatedFiles.map((f) => ({ objid: f.objid, name: f.realFileName })),
|
||||||
});
|
});
|
||||||
|
|
||||||
setUploadedFiles(updatedFiles);
|
setUploadedFiles(updatedFiles);
|
||||||
setUploadStatus('success');
|
setUploadStatus("success");
|
||||||
|
|
||||||
// localStorage 백업
|
// localStorage 백업
|
||||||
try {
|
try {
|
||||||
|
|
@ -633,7 +636,7 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
// 전역 상태 업데이트 (모든 파일 컴포넌트 동기화)
|
// 전역 상태 업데이트 (모든 파일 컴포넌트 동기화)
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== "undefined") {
|
||||||
// 전역 파일 상태 업데이트
|
// 전역 파일 상태 업데이트
|
||||||
const globalFileState = (window as any).globalFileState || {};
|
const globalFileState = (window as any).globalFileState || {};
|
||||||
globalFileState[component.id] = updatedFiles;
|
globalFileState[component.id] = updatedFiles;
|
||||||
|
|
@ -647,23 +650,23 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
});
|
});
|
||||||
|
|
||||||
// 모든 파일 컴포넌트에 동기화 이벤트 발생
|
// 모든 파일 컴포넌트에 동기화 이벤트 발생
|
||||||
const syncEvent = new CustomEvent('globalFileStateChanged', {
|
const syncEvent = new CustomEvent("globalFileStateChanged", {
|
||||||
detail: {
|
detail: {
|
||||||
componentId: component.id,
|
componentId: component.id,
|
||||||
files: updatedFiles,
|
files: updatedFiles,
|
||||||
fileCount: updatedFiles.length,
|
fileCount: updatedFiles.length,
|
||||||
timestamp: Date.now()
|
timestamp: Date.now(),
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
window.dispatchEvent(syncEvent);
|
window.dispatchEvent(syncEvent);
|
||||||
|
|
||||||
console.log("🌐 전역 파일 상태 업데이트 및 동기화 이벤트 발생:", {
|
console.log("🌐 전역 파일 상태 업데이트 및 동기화 이벤트 발생:", {
|
||||||
componentId: component.id,
|
componentId: component.id,
|
||||||
fileCount: updatedFiles.length,
|
fileCount: updatedFiles.length,
|
||||||
globalState: Object.keys(globalFileState).map(id => ({
|
globalState: Object.keys(globalFileState).map((id) => ({
|
||||||
id,
|
id,
|
||||||
fileCount: globalFileState[id]?.length || 0
|
fileCount: globalFileState[id]?.length || 0,
|
||||||
}))
|
})),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -673,26 +676,26 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
console.log("🔄 onUpdate 호출:", {
|
console.log("🔄 onUpdate 호출:", {
|
||||||
componentId: component.id,
|
componentId: component.id,
|
||||||
uploadedFiles: updatedFiles.length,
|
uploadedFiles: updatedFiles.length,
|
||||||
timestamp: timestamp
|
timestamp: timestamp,
|
||||||
});
|
});
|
||||||
onUpdate({
|
onUpdate({
|
||||||
uploadedFiles: updatedFiles,
|
uploadedFiles: updatedFiles,
|
||||||
lastFileUpdate: timestamp
|
lastFileUpdate: timestamp,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.warn("⚠️ onUpdate 콜백이 없습니다!");
|
console.warn("⚠️ onUpdate 콜백이 없습니다!");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 그리드 파일 상태 새로고침 이벤트 발생
|
// 그리드 파일 상태 새로고침 이벤트 발생
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== "undefined") {
|
||||||
const refreshEvent = new CustomEvent('refreshFileStatus', {
|
const refreshEvent = new CustomEvent("refreshFileStatus", {
|
||||||
detail: {
|
detail: {
|
||||||
tableName: tableName,
|
tableName: tableName,
|
||||||
recordId: recordId,
|
recordId: recordId,
|
||||||
columnName: columnName,
|
columnName: columnName,
|
||||||
targetObjid: targetObjid,
|
targetObjid: targetObjid,
|
||||||
fileCount: updatedFiles.length
|
fileCount: updatedFiles.length,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
window.dispatchEvent(refreshEvent);
|
window.dispatchEvent(refreshEvent);
|
||||||
console.log("🔄 그리드 파일 상태 새로고침 이벤트 발생:", {
|
console.log("🔄 그리드 파일 상태 새로고침 이벤트 발생:", {
|
||||||
|
|
@ -700,16 +703,16 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
recordId,
|
recordId,
|
||||||
columnName,
|
columnName,
|
||||||
targetObjid,
|
targetObjid,
|
||||||
fileCount: updatedFiles.length
|
fileCount: updatedFiles.length,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 폼 데이터 업데이트
|
// 폼 데이터 업데이트
|
||||||
if (onFormDataChange && component.columnName) {
|
if (onFormDataChange && component.columnName) {
|
||||||
const fileIds = updatedFiles.map(f => f.objid);
|
const fileIds = updatedFiles.map((f) => f.objid);
|
||||||
onFormDataChange({
|
onFormDataChange({
|
||||||
...formData,
|
...formData,
|
||||||
[component.columnName]: fileIds
|
[component.columnName]: fileIds,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -719,20 +722,22 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
// 성공 시 토스트 처리
|
// 성공 시 토스트 처리
|
||||||
setUploadStatus('idle');
|
setUploadStatus("idle");
|
||||||
toast.dismiss('file-upload');
|
toast.dismiss("file-upload");
|
||||||
toast.success(`${newFiles.length}개 파일 업로드 완료`);
|
toast.success(`${newFiles.length}개 파일 업로드 완료`);
|
||||||
} else {
|
} else {
|
||||||
console.error("❌ 파일 업로드 실패:", response);
|
console.error("❌ 파일 업로드 실패:", response);
|
||||||
throw new Error(response.message || (response as any).error || '파일 업로드에 실패했습니다.');
|
throw new Error(response.message || (response as any).error || "파일 업로드에 실패했습니다.");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('파일 업로드 오류:', error);
|
console.error("파일 업로드 오류:", error);
|
||||||
setUploadStatus('error');
|
setUploadStatus("error");
|
||||||
toast.dismiss('file-upload');
|
toast.dismiss("file-upload");
|
||||||
toast.error(`파일 업로드 오류: ${error instanceof Error ? error.message : '알 수 없는 오류'}`);
|
toast.error(`파일 업로드 오류: ${error instanceof Error ? error.message : "알 수 없는 오류"}`);
|
||||||
}
|
}
|
||||||
}, [safeComponentConfig, uploadedFiles, onFormDataChange, component.columnName, component.id, formData]);
|
},
|
||||||
|
[safeComponentConfig, uploadedFiles, onFormDataChange, component.columnName, component.id, formData],
|
||||||
|
);
|
||||||
|
|
||||||
// 파일 뷰어 열기
|
// 파일 뷰어 열기
|
||||||
const handleFileView = useCallback((file: FileInfo) => {
|
const handleFileView = useCallback((file: FileInfo) => {
|
||||||
|
|
@ -752,25 +757,26 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
await downloadFile({
|
await downloadFile({
|
||||||
fileId: file.objid,
|
fileId: file.objid,
|
||||||
serverFilename: file.savedFileName,
|
serverFilename: file.savedFileName,
|
||||||
originalName: file.realFileName
|
originalName: file.realFileName,
|
||||||
});
|
});
|
||||||
toast.success(`${file.realFileName} 다운로드 완료`);
|
toast.success(`${file.realFileName} 다운로드 완료`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('파일 다운로드 오류:', error);
|
console.error("파일 다운로드 오류:", error);
|
||||||
toast.error('파일 다운로드에 실패했습니다.');
|
toast.error("파일 다운로드에 실패했습니다.");
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 파일 삭제
|
// 파일 삭제
|
||||||
const handleFileDelete = useCallback(async (file: FileInfo | string) => {
|
const handleFileDelete = useCallback(
|
||||||
|
async (file: FileInfo | string) => {
|
||||||
try {
|
try {
|
||||||
const fileId = typeof file === 'string' ? file : file.objid;
|
const fileId = typeof file === "string" ? file : file.objid;
|
||||||
const fileName = typeof file === 'string' ? '파일' : file.realFileName;
|
const fileName = typeof file === "string" ? "파일" : file.realFileName;
|
||||||
|
|
||||||
const serverFilename = typeof file === 'string' ? 'temp_file' : file.savedFileName;
|
const serverFilename = typeof file === "string" ? "temp_file" : file.savedFileName;
|
||||||
await deleteFile(fileId, serverFilename);
|
await deleteFile(fileId, serverFilename);
|
||||||
|
|
||||||
const updatedFiles = uploadedFiles.filter(f => f.objid !== fileId);
|
const updatedFiles = uploadedFiles.filter((f) => f.objid !== fileId);
|
||||||
setUploadedFiles(updatedFiles);
|
setUploadedFiles(updatedFiles);
|
||||||
|
|
||||||
// localStorage 백업 업데이트
|
// localStorage 백업 업데이트
|
||||||
|
|
@ -782,29 +788,29 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
// 전역 상태 업데이트 (모든 파일 컴포넌트 동기화)
|
// 전역 상태 업데이트 (모든 파일 컴포넌트 동기화)
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== "undefined") {
|
||||||
// 전역 파일 상태 업데이트
|
// 전역 파일 상태 업데이트
|
||||||
const globalFileState = (window as any).globalFileState || {};
|
const globalFileState = (window as any).globalFileState || {};
|
||||||
globalFileState[component.id] = updatedFiles;
|
globalFileState[component.id] = updatedFiles;
|
||||||
(window as any).globalFileState = globalFileState;
|
(window as any).globalFileState = globalFileState;
|
||||||
|
|
||||||
// 모든 파일 컴포넌트에 동기화 이벤트 발생
|
// 모든 파일 컴포넌트에 동기화 이벤트 발생
|
||||||
const syncEvent = new CustomEvent('globalFileStateChanged', {
|
const syncEvent = new CustomEvent("globalFileStateChanged", {
|
||||||
detail: {
|
detail: {
|
||||||
componentId: component.id,
|
componentId: component.id,
|
||||||
files: updatedFiles,
|
files: updatedFiles,
|
||||||
fileCount: updatedFiles.length,
|
fileCount: updatedFiles.length,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
source: 'realScreen', // 🎯 실제 화면에서 온 이벤트임을 표시
|
source: "realScreen", // 🎯 실제 화면에서 온 이벤트임을 표시
|
||||||
action: 'delete'
|
action: "delete",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
window.dispatchEvent(syncEvent);
|
window.dispatchEvent(syncEvent);
|
||||||
|
|
||||||
console.log("🗑️ 파일 삭제 후 전역 상태 동기화:", {
|
console.log("🗑️ 파일 삭제 후 전역 상태 동기화:", {
|
||||||
componentId: component.id,
|
componentId: component.id,
|
||||||
deletedFile: fileName,
|
deletedFile: fileName,
|
||||||
remainingFiles: updatedFiles.length
|
remainingFiles: updatedFiles.length,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -813,23 +819,26 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
const timestamp = Date.now();
|
const timestamp = Date.now();
|
||||||
onUpdate({
|
onUpdate({
|
||||||
uploadedFiles: updatedFiles,
|
uploadedFiles: updatedFiles,
|
||||||
lastFileUpdate: timestamp
|
lastFileUpdate: timestamp,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.success(`${fileName} 삭제 완료`);
|
toast.success(`${fileName} 삭제 완료`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('파일 삭제 오류:', error);
|
console.error("파일 삭제 오류:", error);
|
||||||
toast.error('파일 삭제에 실패했습니다.');
|
toast.error("파일 삭제에 실패했습니다.");
|
||||||
}
|
}
|
||||||
}, [uploadedFiles, onUpdate, component.id]);
|
},
|
||||||
|
[uploadedFiles, onUpdate, component.id],
|
||||||
|
);
|
||||||
|
|
||||||
// 드래그 앤 드롭 핸들러
|
// 드래그 앤 드롭 핸들러
|
||||||
const handleDragOver = useCallback((e: React.DragEvent) => {
|
const handleDragOver = useCallback(
|
||||||
|
(e: React.DragEvent) => {
|
||||||
console.log("🎯 드래그 오버 이벤트 감지:", {
|
console.log("🎯 드래그 오버 이벤트 감지:", {
|
||||||
readonly: safeComponentConfig.readonly,
|
readonly: safeComponentConfig.readonly,
|
||||||
disabled: safeComponentConfig.disabled,
|
disabled: safeComponentConfig.disabled,
|
||||||
dragOver: dragOver
|
dragOver: dragOver,
|
||||||
});
|
});
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -840,7 +849,9 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
} else {
|
} else {
|
||||||
console.log("❌ 드래그 차단됨: readonly 또는 disabled");
|
console.log("❌ 드래그 차단됨: readonly 또는 disabled");
|
||||||
}
|
}
|
||||||
}, [safeComponentConfig.readonly, safeComponentConfig.disabled, dragOver]);
|
},
|
||||||
|
[safeComponentConfig.readonly, safeComponentConfig.disabled, dragOver],
|
||||||
|
);
|
||||||
|
|
||||||
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -848,7 +859,8 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
setDragOver(false);
|
setDragOver(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleDrop = useCallback((e: React.DragEvent) => {
|
const handleDrop = useCallback(
|
||||||
|
(e: React.DragEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setDragOver(false);
|
setDragOver(false);
|
||||||
|
|
@ -859,14 +871,17 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
handleFileUpload(files);
|
handleFileUpload(files);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [safeComponentConfig.readonly, safeComponentConfig.disabled, handleFileUpload]);
|
},
|
||||||
|
[safeComponentConfig.readonly, safeComponentConfig.disabled, handleFileUpload],
|
||||||
|
);
|
||||||
|
|
||||||
// 클릭 핸들러
|
// 클릭 핸들러
|
||||||
const handleClick = useCallback((e: React.MouseEvent) => {
|
const handleClick = useCallback(
|
||||||
|
(e: React.MouseEvent) => {
|
||||||
console.log("🖱️ 파일 업로드 영역 클릭:", {
|
console.log("🖱️ 파일 업로드 영역 클릭:", {
|
||||||
readonly: safeComponentConfig.readonly,
|
readonly: safeComponentConfig.readonly,
|
||||||
disabled: safeComponentConfig.disabled,
|
disabled: safeComponentConfig.disabled,
|
||||||
hasHandleFileSelect: !!handleFileSelect
|
hasHandleFileSelect: !!handleFileSelect,
|
||||||
});
|
});
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -878,19 +893,21 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
console.log("❌ 클릭 차단됨: readonly 또는 disabled");
|
console.log("❌ 클릭 차단됨: readonly 또는 disabled");
|
||||||
}
|
}
|
||||||
onClick?.();
|
onClick?.();
|
||||||
}, [safeComponentConfig.readonly, safeComponentConfig.disabled, handleFileSelect, onClick]);
|
},
|
||||||
|
[safeComponentConfig.readonly, safeComponentConfig.disabled, handleFileSelect, onClick],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
...componentStyle,
|
...componentStyle,
|
||||||
border: 'none !important',
|
border: "none !important",
|
||||||
boxShadow: 'none !important',
|
boxShadow: "none !important",
|
||||||
outline: 'none !important',
|
outline: "none !important",
|
||||||
backgroundColor: 'transparent !important',
|
backgroundColor: "transparent !important",
|
||||||
padding: '0px !important',
|
padding: "0px !important",
|
||||||
borderRadius: '0px !important',
|
borderRadius: "0px !important",
|
||||||
marginBottom: '8px !important'
|
marginBottom: "8px !important",
|
||||||
}}
|
}}
|
||||||
className={`${className} file-upload-container`}
|
className={`${className} file-upload-container`}
|
||||||
>
|
>
|
||||||
|
|
@ -920,8 +937,8 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
)} */}
|
)} */}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="w-full h-full flex flex-col space-y-3 p-4 border border-gray-200 rounded-lg bg-gradient-to-br from-gray-50 to-white shadow-sm hover:shadow-md transition-all duration-200"
|
className="border-border bg-card flex h-full w-full flex-col space-y-3 rounded-lg border p-3 transition-all duration-200 hover:shadow-sm"
|
||||||
style={{ minHeight: '140px' }}
|
style={{ minHeight: "140px" }}
|
||||||
>
|
>
|
||||||
{/* 파일 업로드 영역 - 주석처리 */}
|
{/* 파일 업로드 영역 - 주석처리 */}
|
||||||
{/* {!isDesignMode && (
|
{/* {!isDesignMode && (
|
||||||
|
|
@ -977,21 +994,21 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
uploadedFilesLength: uploadedFiles.length,
|
uploadedFilesLength: uploadedFiles.length,
|
||||||
isDesignMode: isDesignMode,
|
isDesignMode: isDesignMode,
|
||||||
shouldShow: shouldShow,
|
shouldShow: shouldShow,
|
||||||
uploadedFiles: uploadedFiles.map(f => ({ objid: f.objid, name: f.realFileName })),
|
uploadedFiles: uploadedFiles.map((f) => ({ objid: f.objid, name: f.realFileName })),
|
||||||
"🚨 렌더링 여부": shouldShow ? "✅ 렌더링됨" : "❌ 렌더링 안됨"
|
"🚨 렌더링 여부": shouldShow ? "✅ 렌더링됨" : "❌ 렌더링 안됨",
|
||||||
});
|
});
|
||||||
return shouldShow;
|
return shouldShow;
|
||||||
})() && (
|
})() && (
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div className="flex-1 overflow-y-auto">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h4 className="text-base font-semibold text-gray-800 flex items-center gap-2">
|
<h4 className="flex items-center gap-2 text-sm font-semibold">
|
||||||
<File className="w-5 h-5 text-blue-500" />
|
<File className="text-primary h-4 w-4" />
|
||||||
업로드된 파일 ({uploadedFiles.length})
|
업로드된 파일 ({uploadedFiles.length})
|
||||||
</h4>
|
</h4>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
{uploadedFiles.length > 0 && (
|
{uploadedFiles.length > 0 && (
|
||||||
<Badge variant="secondary" className="text-xs">
|
<Badge variant="secondary" className="h-5 px-1.5 text-xs">
|
||||||
총 {formatFileSize(uploadedFiles.reduce((sum, file) => sum + file.fileSize, 0))}
|
총 {formatFileSize(uploadedFiles.reduce((sum, file) => sum + file.fileSize, 0))}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
|
@ -1001,11 +1018,11 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
className="h-7 px-2 text-xs"
|
className="h-7 px-2 text-xs"
|
||||||
onClick={() => setIsFileManagerOpen(true)}
|
onClick={() => setIsFileManagerOpen(true)}
|
||||||
style={{
|
style={{
|
||||||
boxShadow: 'none !important',
|
boxShadow: "none !important",
|
||||||
textShadow: 'none !important',
|
textShadow: "none !important",
|
||||||
filter: 'none !important',
|
filter: "none !important",
|
||||||
WebkitBoxShadow: 'none !important',
|
WebkitBoxShadow: "none !important",
|
||||||
MozBoxShadow: 'none !important'
|
MozBoxShadow: "none !important",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
자세히보기
|
자세히보기
|
||||||
|
|
@ -1016,27 +1033,40 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
{uploadedFiles.length > 0 ? (
|
{uploadedFiles.length > 0 ? (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{uploadedFiles.map((file) => (
|
{uploadedFiles.map((file) => (
|
||||||
<div key={file.objid} className="flex items-center space-x-3 p-2 bg-gray-50 rounded text-sm hover:bg-gray-100 transition-colors" style={{ boxShadow: 'none', textShadow: 'none' }}>
|
<div
|
||||||
<div className="flex-shrink-0">
|
key={file.objid}
|
||||||
{getFileIcon(file.fileExt)}
|
className="hover:bg-accent flex items-center space-x-3 rounded p-2 text-sm transition-colors"
|
||||||
</div>
|
style={{ boxShadow: "none", textShadow: "none" }}
|
||||||
<span className="flex-1 truncate text-gray-900 cursor-pointer" onClick={() => handleFileView(file)} style={{ textShadow: 'none' }}>
|
>
|
||||||
|
<div className="flex-shrink-0">{getFileIcon(file.fileExt)}</div>
|
||||||
|
<span
|
||||||
|
className="flex-1 cursor-pointer truncate text-gray-900"
|
||||||
|
onClick={() => handleFileView(file)}
|
||||||
|
style={{ textShadow: "none" }}
|
||||||
|
>
|
||||||
{file.realFileName}
|
{file.realFileName}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-gray-500" style={{ textShadow: 'none' }}>
|
<span className="text-xs text-gray-500" style={{ textShadow: "none" }}>
|
||||||
{formatFileSize(file.fileSize)}
|
{formatFileSize(file.fileSize)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div className="text-xs text-gray-500 mt-2 text-center" style={{ textShadow: 'none' }}>
|
<div className="mt-2 text-center text-xs text-gray-500" style={{ textShadow: "none" }}>
|
||||||
💡 파일명 클릭으로 미리보기 또는 "전체 자세히보기"로 파일 관리
|
💡 파일명 클릭으로 미리보기 또는 "전체 자세히보기"로 파일 관리
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center justify-center py-8 text-gray-500" style={{ textShadow: 'none' }}>
|
<div
|
||||||
<File className="w-12 h-12 mb-3 text-gray-300" />
|
className="flex flex-col items-center justify-center py-8 text-gray-500"
|
||||||
<p className="text-sm font-medium" style={{ textShadow: 'none' }}>업로드된 파일이 없습니다</p>
|
style={{ textShadow: "none" }}
|
||||||
<p className="text-xs text-gray-400 mt-1" style={{ textShadow: 'none' }}>상세설정에서 파일을 업로드하세요</p>
|
>
|
||||||
|
<File className="mb-3 h-12 w-12 text-gray-300" />
|
||||||
|
<p className="text-sm font-medium" style={{ textShadow: "none" }}>
|
||||||
|
업로드된 파일이 없습니다
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-xs text-gray-400" style={{ textShadow: "none" }}>
|
||||||
|
상세설정에서 파일을 업로드하세요
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1045,9 +1075,7 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
|
|
||||||
{/* 도움말 텍스트 */}
|
{/* 도움말 텍스트 */}
|
||||||
{safeComponentConfig.helperText && (
|
{safeComponentConfig.helperText && (
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
<p className="mt-1 text-xs text-gray-500">{safeComponentConfig.helperText}</p>
|
||||||
{safeComponentConfig.helperText}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -138,9 +138,9 @@ export const RadioBasicComponent: React.FC<RadioBasicComponentProps> = ({
|
||||||
onChange={() => handleRadioChange(option.value)}
|
onChange={() => handleRadioChange(option.value)}
|
||||||
disabled={componentConfig.disabled || isDesignMode}
|
disabled={componentConfig.disabled || isDesignMode}
|
||||||
required={componentConfig.required || false}
|
required={componentConfig.required || false}
|
||||||
className="h-4 w-4 border-gray-300 text-blue-600 focus:ring-0"
|
className="border-input text-primary h-4 w-4 focus:ring-0"
|
||||||
/>
|
/>
|
||||||
<span className="text-sm text-gray-900">{option.label}</span>
|
<span className="text-sm">{option.label}</span>
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -323,9 +323,9 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||||
checked={selectedValue === option.value}
|
checked={selectedValue === option.value}
|
||||||
onChange={() => handleOptionSelect(option.value, option.label)}
|
onChange={() => handleOptionSelect(option.value, option.label)}
|
||||||
disabled={isDesignMode}
|
disabled={isDesignMode}
|
||||||
className="h-4 w-4 border-gray-300 text-blue-600 focus:ring-blue-500"
|
className="border-input text-primary focus:ring-ring h-4 w-4"
|
||||||
/>
|
/>
|
||||||
<span className="text-sm text-gray-900">{option.label}</span>
|
<span className="text-sm">{option.label}</span>
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -277,10 +277,10 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
{/* 좌측 패널 */}
|
{/* 좌측 패널 */}
|
||||||
<div
|
<div
|
||||||
style={{ width: `${leftWidth}%`, minWidth: isPreview ? "0" : `${minLeftWidth}px` }}
|
style={{ width: `${leftWidth}%`, minWidth: isPreview ? "0" : `${minLeftWidth}px` }}
|
||||||
className="flex flex-shrink-0 flex-col border-r border-gray-200"
|
className="border-border flex flex-shrink-0 flex-col border-r"
|
||||||
>
|
>
|
||||||
<Card className="flex h-full flex-col border-0 shadow-none">
|
<Card className="flex h-full flex-col border-0 shadow-none">
|
||||||
<CardHeader className="border-b border-gray-100 pb-3">
|
<CardHeader className="border-b pb-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<CardTitle className="text-base font-semibold">
|
<CardTitle className="text-base font-semibold">
|
||||||
{componentConfig.leftPanel?.title || "좌측 패널"}
|
{componentConfig.leftPanel?.title || "좌측 패널"}
|
||||||
|
|
@ -312,37 +312,37 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
onClick={() => handleLeftItemSelect({ id: 1, name: "항목 1" })}
|
onClick={() => handleLeftItemSelect({ id: 1, name: "항목 1" })}
|
||||||
className={`cursor-pointer rounded-md p-3 transition-colors hover:bg-gray-50 ${
|
className={`hover:bg-accent cursor-pointer rounded-md p-3 transition-colors ${
|
||||||
selectedLeftItem?.id === 1 ? "bg-blue-50 text-blue-700" : "text-gray-700"
|
selectedLeftItem?.id === 1 ? "bg-primary/10 text-primary" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="font-medium">항목 1</div>
|
<div className="font-medium">항목 1</div>
|
||||||
<div className="text-xs text-gray-500">설명 텍스트</div>
|
<div className="text-muted-foreground text-xs">설명 텍스트</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
onClick={() => handleLeftItemSelect({ id: 2, name: "항목 2" })}
|
onClick={() => handleLeftItemSelect({ id: 2, name: "항목 2" })}
|
||||||
className={`cursor-pointer rounded-md p-3 transition-colors hover:bg-gray-50 ${
|
className={`hover:bg-accent cursor-pointer rounded-md p-3 transition-colors ${
|
||||||
selectedLeftItem?.id === 2 ? "bg-blue-50 text-blue-700" : "text-gray-700"
|
selectedLeftItem?.id === 2 ? "bg-primary/10 text-primary" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="font-medium">항목 2</div>
|
<div className="font-medium">항목 2</div>
|
||||||
<div className="text-xs text-gray-500">설명 텍스트</div>
|
<div className="text-muted-foreground text-xs">설명 텍스트</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
onClick={() => handleLeftItemSelect({ id: 3, name: "항목 3" })}
|
onClick={() => handleLeftItemSelect({ id: 3, name: "항목 3" })}
|
||||||
className={`cursor-pointer rounded-md p-3 transition-colors hover:bg-gray-50 ${
|
className={`hover:bg-accent cursor-pointer rounded-md p-3 transition-colors ${
|
||||||
selectedLeftItem?.id === 3 ? "bg-blue-50 text-blue-700" : "text-gray-700"
|
selectedLeftItem?.id === 3 ? "bg-primary/10 text-primary" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="font-medium">항목 3</div>
|
<div className="font-medium">항목 3</div>
|
||||||
<div className="text-xs text-gray-500">설명 텍스트</div>
|
<div className="text-muted-foreground text-xs">설명 텍스트</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : isLoadingLeft ? (
|
) : isLoadingLeft ? (
|
||||||
// 로딩 중
|
// 로딩 중
|
||||||
<div className="flex items-center justify-center py-8">
|
<div className="flex items-center justify-center py-8">
|
||||||
<Loader2 className="h-6 w-6 animate-spin text-blue-500" />
|
<Loader2 className="text-primary h-6 w-6 animate-spin" />
|
||||||
<span className="ml-2 text-sm text-gray-500">데이터를 불러오는 중...</span>
|
<span className="text-muted-foreground ml-2 text-sm">데이터를 불러오는 중...</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
(() => {
|
(() => {
|
||||||
|
|
@ -417,7 +417,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
className="flex flex-shrink-0 flex-col"
|
className="flex flex-shrink-0 flex-col"
|
||||||
>
|
>
|
||||||
<Card className="flex h-full flex-col border-0 shadow-none">
|
<Card className="flex h-full flex-col border-0 shadow-none">
|
||||||
<CardHeader className="border-b border-gray-100 pb-3">
|
<CardHeader className="border-b pb-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<CardTitle className="text-base font-semibold">
|
<CardTitle className="text-base font-semibold">
|
||||||
{componentConfig.rightPanel?.title || "우측 패널"}
|
{componentConfig.rightPanel?.title || "우측 패널"}
|
||||||
|
|
@ -488,7 +488,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={itemId}
|
key={itemId}
|
||||||
className="overflow-hidden rounded-lg border border-gray-200 bg-white shadow-sm transition-all hover:shadow-md"
|
className="bg-card overflow-hidden rounded-lg border shadow-sm transition-all hover:shadow-md"
|
||||||
>
|
>
|
||||||
{/* 요약 정보 (클릭 가능) */}
|
{/* 요약 정보 (클릭 가능) */}
|
||||||
<div
|
<div
|
||||||
|
|
@ -518,9 +518,9 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
|
|
||||||
{/* 상세 정보 (확장 시 표시) */}
|
{/* 상세 정보 (확장 시 표시) */}
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<div className="border-t border-gray-200 bg-gray-50 px-3 py-2">
|
<div className="bg-muted/50 border-t px-3 py-2">
|
||||||
<div className="mb-2 text-xs font-semibold text-gray-700">전체 상세 정보</div>
|
<div className="mb-2 text-xs font-semibold">전체 상세 정보</div>
|
||||||
<div className="overflow-auto rounded-md border border-gray-200 bg-white">
|
<div className="bg-card overflow-auto rounded-md border">
|
||||||
<table className="w-full text-sm">
|
<table className="w-full text-sm">
|
||||||
<tbody className="divide-y divide-gray-200">
|
<tbody className="divide-y divide-gray-200">
|
||||||
{allValues.map(([key, value]) => (
|
{allValues.map(([key, value]) => (
|
||||||
|
|
@ -561,9 +561,11 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
if (value === null || value === undefined || value === "") return null;
|
if (value === null || value === undefined || value === "") return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={key} className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm">
|
<div key={key} className="bg-card rounded-lg border p-4 shadow-sm">
|
||||||
<div className="mb-1 text-xs font-semibold tracking-wide text-gray-500 uppercase">{key}</div>
|
<div className="text-muted-foreground mb-1 text-xs font-semibold tracking-wide uppercase">
|
||||||
<div className="text-sm text-gray-900">{String(value)}</div>
|
{key}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm">{String(value)}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
@ -572,8 +574,8 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
) : selectedLeftItem && isDesignMode ? (
|
) : selectedLeftItem && isDesignMode ? (
|
||||||
// 디자인 모드: 샘플 데이터
|
// 디자인 모드: 샘플 데이터
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="rounded-lg border border-gray-200 p-4">
|
<div className="rounded-lg border p-4">
|
||||||
<h3 className="mb-2 font-medium text-gray-900">{selectedLeftItem.name} 상세 정보</h3>
|
<h3 className="mb-2 font-medium">{selectedLeftItem.name} 상세 정보</h3>
|
||||||
<div className="space-y-2 text-sm">
|
<div className="space-y-2 text-sm">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-gray-600">항목 1:</span>
|
<span className="text-gray-600">항목 1:</span>
|
||||||
|
|
|
||||||
|
|
@ -1361,7 +1361,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
{tableConfig.showHeader && (
|
{tableConfig.showHeader && (
|
||||||
<div
|
<div
|
||||||
className="flex items-center justify-between border-b border-gray-200/40 bg-gradient-to-r from-slate-50/80 to-gray-50/60 px-6 py-5"
|
className="bg-muted/30 flex items-center justify-between border-b px-6 py-5"
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
maxWidth: "100%",
|
maxWidth: "100%",
|
||||||
|
|
@ -1388,7 +1388,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleRefresh}
|
onClick={handleRefresh}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="group relative rounded-xl border-gray-200/60 bg-white/80 shadow-sm backdrop-blur-sm transition-all duration-200 hover:bg-gray-50/80 hover:shadow-md"
|
className="group relative rounded-xl shadow-sm transition-all duration-200 hover:shadow-md"
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
|
@ -1699,7 +1699,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
{/* showFooter와 pagination.enabled의 기본값은 true */}
|
{/* showFooter와 pagination.enabled의 기본값은 true */}
|
||||||
{tableConfig.showFooter !== false && tableConfig.pagination?.enabled !== false && (
|
{tableConfig.showFooter !== false && tableConfig.pagination?.enabled !== false && (
|
||||||
<div
|
<div
|
||||||
className="flex flex-col items-center justify-center space-y-4 border-t border-gray-200 bg-gray-100/80 p-6"
|
className="bg-muted/30 flex flex-col items-center justify-center space-y-4 border-t p-6"
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
maxWidth: "100%",
|
maxWidth: "100%",
|
||||||
|
|
@ -1759,7 +1759,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
|
|
||||||
// 데이터는 useEffect에서 자동으로 다시 로드됨
|
// 데이터는 useEffect에서 자동으로 다시 로드됨
|
||||||
}}
|
}}
|
||||||
className="rounded-xl border border-gray-200/60 bg-white/80 px-4 py-2 text-sm font-medium text-gray-700 shadow-sm backdrop-blur-sm transition-all duration-200 hover:border-gray-300/60 hover:bg-white hover:shadow-md"
|
className="rounded-xl border px-4 py-2 text-sm font-medium shadow-sm transition-all duration-200 hover:shadow-md"
|
||||||
>
|
>
|
||||||
{(tableConfig.pagination?.pageSizeOptions || [10, 20, 50, 100]).map((size) => (
|
{(tableConfig.pagination?.pageSizeOptions || [10, 20, 50, 100]).map((size) => (
|
||||||
<option key={size} value={size}>
|
<option key={size} value={size}>
|
||||||
|
|
@ -1770,7 +1770,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 페이지네이션 버튼 */}
|
{/* 페이지네이션 버튼 */}
|
||||||
<div className="flex items-center space-x-2 rounded-xl border border-gray-200/60 bg-white/80 p-1 shadow-sm backdrop-blur-sm">
|
<div className="flex items-center space-x-2 rounded-xl border p-1 shadow-sm">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
@ -1790,7 +1790,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
<ChevronLeft className="h-4 w-4" />
|
<ChevronLeft className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<div className="flex items-center rounded-lg border border-gray-200/40 bg-gradient-to-r from-gray-50/80 to-slate-50/60 px-4 py-2 backdrop-blur-sm">
|
<div className="bg-muted/30 flex items-center rounded-lg border px-4 py-2">
|
||||||
<span className="text-sm font-semibold text-gray-800">{currentPage}</span>
|
<span className="text-sm font-semibold text-gray-800">{currentPage}</span>
|
||||||
<span className="mx-2 font-light text-gray-400">/</span>
|
<span className="mx-2 font-light text-gray-400">/</span>
|
||||||
<span className="text-sm font-medium text-gray-600">{totalPages}</span>
|
<span className="text-sm font-medium text-gray-600">{totalPages}</span>
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,14 @@
|
||||||
"": {
|
"": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^6.16.2",
|
"@prisma/client": "^6.16.2",
|
||||||
|
"@react-three/drei": "^10.7.6",
|
||||||
|
"@react-three/fiber": "^9.4.0",
|
||||||
"@types/mssql": "^9.1.8",
|
"@types/mssql": "^9.1.8",
|
||||||
"@xyflow/react": "^12.8.6",
|
"@xyflow/react": "^12.8.6",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
"mssql": "^11.0.1",
|
"mssql": "^11.0.1",
|
||||||
"prisma": "^6.16.2"
|
"prisma": "^6.16.2",
|
||||||
|
"three": "^0.180.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/oracledb": "^6.9.1",
|
"@types/oracledb": "^6.9.1",
|
||||||
|
|
@ -275,12 +278,45 @@
|
||||||
"node": ">=16"
|
"node": ">=16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/runtime": {
|
||||||
|
"version": "7.28.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
|
||||||
|
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dimforge/rapier3d-compat": {
|
||||||
|
"version": "0.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
|
||||||
|
"integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/@js-joda/core": {
|
"node_modules/@js-joda/core": {
|
||||||
"version": "5.6.5",
|
"version": "5.6.5",
|
||||||
"resolved": "https://registry.npmjs.org/@js-joda/core/-/core-5.6.5.tgz",
|
"resolved": "https://registry.npmjs.org/@js-joda/core/-/core-5.6.5.tgz",
|
||||||
"integrity": "sha512-3zwefSMwHpu8iVUW8YYz227sIv6UFqO31p1Bf1ZH/Vom7CmNyUsXjDBlnNzcuhmOL1XfxZ3nvND42kR23XlbcQ==",
|
"integrity": "sha512-3zwefSMwHpu8iVUW8YYz227sIv6UFqO31p1Bf1ZH/Vom7CmNyUsXjDBlnNzcuhmOL1XfxZ3nvND42kR23XlbcQ==",
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
|
"node_modules/@mediapipe/tasks-vision": {
|
||||||
|
"version": "0.10.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.17.tgz",
|
||||||
|
"integrity": "sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/@monogrid/gainmap-js": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-Obb0/gEd/HReTlg8ttaYk+0m62gQJmCblMOjHSMHRrBP2zdfKMHLCRbh/6ex9fSUJMKdjjIEiohwkbGD3wj2Nw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"promise-worker-transferable": "^1.0.4"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"three": ">= 0.159.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@prisma/client": {
|
"node_modules/@prisma/client": {
|
||||||
"version": "6.16.2",
|
"version": "6.16.2",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.16.2.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.16.2.tgz",
|
||||||
|
|
@ -360,6 +396,160 @@
|
||||||
"@prisma/debug": "6.16.2"
|
"@prisma/debug": "6.16.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@react-three/drei": {
|
||||||
|
"version": "10.7.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-10.7.6.tgz",
|
||||||
|
"integrity": "sha512-ZSFwRlRaa4zjtB7yHO6Q9xQGuyDCzE7whXBhum92JslcMRC3aouivp0rAzszcVymIoJx6PXmibyP+xr+zKdwLg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.26.0",
|
||||||
|
"@mediapipe/tasks-vision": "0.10.17",
|
||||||
|
"@monogrid/gainmap-js": "^3.0.6",
|
||||||
|
"@use-gesture/react": "^10.3.1",
|
||||||
|
"camera-controls": "^3.1.0",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"detect-gpu": "^5.0.56",
|
||||||
|
"glsl-noise": "^0.0.0",
|
||||||
|
"hls.js": "^1.5.17",
|
||||||
|
"maath": "^0.10.8",
|
||||||
|
"meshline": "^3.3.1",
|
||||||
|
"stats-gl": "^2.2.8",
|
||||||
|
"stats.js": "^0.17.0",
|
||||||
|
"suspend-react": "^0.1.3",
|
||||||
|
"three-mesh-bvh": "^0.8.3",
|
||||||
|
"three-stdlib": "^2.35.6",
|
||||||
|
"troika-three-text": "^0.52.4",
|
||||||
|
"tunnel-rat": "^0.1.2",
|
||||||
|
"use-sync-external-store": "^1.4.0",
|
||||||
|
"utility-types": "^3.11.0",
|
||||||
|
"zustand": "^5.0.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@react-three/fiber": "^9.0.0",
|
||||||
|
"react": "^19",
|
||||||
|
"react-dom": "^19",
|
||||||
|
"three": ">=0.159"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@react-three/drei/node_modules/zustand": {
|
||||||
|
"version": "5.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz",
|
||||||
|
"integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.20.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": ">=18.0.0",
|
||||||
|
"immer": ">=9.0.6",
|
||||||
|
"react": ">=18.0.0",
|
||||||
|
"use-sync-external-store": ">=1.2.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"immer": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"use-sync-external-store": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@react-three/fiber": {
|
||||||
|
"version": "9.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.4.0.tgz",
|
||||||
|
"integrity": "sha512-k4iu1R6e5D54918V4sqmISUkI5OgTw3v7/sDRKEC632Wd5g2WBtUS5gyG63X0GJO/HZUj1tsjSXfyzwrUHZl1g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.17.8",
|
||||||
|
"@types/react-reconciler": "^0.32.0",
|
||||||
|
"@types/webxr": "*",
|
||||||
|
"base64-js": "^1.5.1",
|
||||||
|
"buffer": "^6.0.3",
|
||||||
|
"its-fine": "^2.0.0",
|
||||||
|
"react-reconciler": "^0.31.0",
|
||||||
|
"react-use-measure": "^2.1.7",
|
||||||
|
"scheduler": "^0.25.0",
|
||||||
|
"suspend-react": "^0.1.3",
|
||||||
|
"use-sync-external-store": "^1.4.0",
|
||||||
|
"zustand": "^5.0.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": ">=43.0",
|
||||||
|
"expo-asset": ">=8.4",
|
||||||
|
"expo-file-system": ">=11.0",
|
||||||
|
"expo-gl": ">=11.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"react-native": ">=0.78",
|
||||||
|
"three": ">=0.156"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"expo": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"expo-asset": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"expo-file-system": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"expo-gl": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-native": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@react-three/fiber/node_modules/scheduler": {
|
||||||
|
"version": "0.25.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
|
||||||
|
"integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@react-three/fiber/node_modules/zustand": {
|
||||||
|
"version": "5.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz",
|
||||||
|
"integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.20.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": ">=18.0.0",
|
||||||
|
"immer": ">=9.0.6",
|
||||||
|
"react": ">=18.0.0",
|
||||||
|
"use-sync-external-store": ">=1.2.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"immer": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"use-sync-external-store": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@standard-schema/spec": {
|
"node_modules/@standard-schema/spec": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
|
||||||
|
|
@ -372,6 +562,12 @@
|
||||||
"integrity": "sha512-7qSgZbincDDDFyRweCIEvZULFAw5iz/DeunhvuxpL31nfntX3P4Yd4HkHBRg9H8CdqY1e5WFN1PZIz/REL9MVQ==",
|
"integrity": "sha512-7qSgZbincDDDFyRweCIEvZULFAw5iz/DeunhvuxpL31nfntX3P4Yd4HkHBRg9H8CdqY1e5WFN1PZIz/REL9MVQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@tweenjs/tween.js": {
|
||||||
|
"version": "23.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
|
||||||
|
"integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/d3-color": {
|
"node_modules/@types/d3-color": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
||||||
|
|
@ -421,6 +617,12 @@
|
||||||
"@types/d3-selection": "*"
|
"@types/d3-selection": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/draco3d": {
|
||||||
|
"version": "1.4.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz",
|
||||||
|
"integrity": "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/mssql": {
|
"node_modules/@types/mssql": {
|
||||||
"version": "9.1.8",
|
"version": "9.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mssql/-/mssql-9.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mssql/-/mssql-9.1.8.tgz",
|
||||||
|
|
@ -441,6 +643,12 @@
|
||||||
"undici-types": "~7.12.0"
|
"undici-types": "~7.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/offscreencanvas": {
|
||||||
|
"version": "2019.7.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz",
|
||||||
|
"integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/oracledb": {
|
"node_modules/@types/oracledb": {
|
||||||
"version": "6.9.1",
|
"version": "6.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/oracledb/-/oracledb-6.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/oracledb/-/oracledb-6.9.1.tgz",
|
||||||
|
|
@ -463,6 +671,25 @@
|
||||||
"pg-types": "^2.2.0"
|
"pg-types": "^2.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/react": {
|
||||||
|
"version": "19.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
|
||||||
|
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"csstype": "^3.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/react-reconciler": {
|
||||||
|
"version": "0.32.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.32.2.tgz",
|
||||||
|
"integrity": "sha512-gjcm6O0aUknhYaogEl8t5pecPfiOTD8VQkbjOhgbZas/E6qGY+veW9iuJU/7p4Y1E0EuQ0mArga7VEOUWSlVRA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/readable-stream": {
|
"node_modules/@types/readable-stream": {
|
||||||
"version": "4.0.21",
|
"version": "4.0.21",
|
||||||
"resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.21.tgz",
|
"resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.21.tgz",
|
||||||
|
|
@ -472,6 +699,33 @@
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/stats.js": {
|
||||||
|
"version": "0.17.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
|
||||||
|
"integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/three": {
|
||||||
|
"version": "0.180.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.180.0.tgz",
|
||||||
|
"integrity": "sha512-ykFtgCqNnY0IPvDro7h+9ZeLY+qjgUWv+qEvUt84grhenO60Hqd4hScHE7VTB9nOQ/3QM8lkbNE+4vKjEpUxKg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@dimforge/rapier3d-compat": "~0.12.0",
|
||||||
|
"@tweenjs/tween.js": "~23.1.3",
|
||||||
|
"@types/stats.js": "*",
|
||||||
|
"@types/webxr": "*",
|
||||||
|
"@webgpu/types": "*",
|
||||||
|
"fflate": "~0.8.2",
|
||||||
|
"meshoptimizer": "~0.22.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/webxr": {
|
||||||
|
"version": "0.5.24",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz",
|
||||||
|
"integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@typespec/ts-http-runtime": {
|
"node_modules/@typespec/ts-http-runtime": {
|
||||||
"version": "0.3.1",
|
"version": "0.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.1.tgz",
|
||||||
|
|
@ -486,6 +740,30 @@
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@use-gesture/core": {
|
||||||
|
"version": "10.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz",
|
||||||
|
"integrity": "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@use-gesture/react": {
|
||||||
|
"version": "10.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.3.1.tgz",
|
||||||
|
"integrity": "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@use-gesture/core": "10.3.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">= 16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@webgpu/types": {
|
||||||
|
"version": "0.1.66",
|
||||||
|
"resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.66.tgz",
|
||||||
|
"integrity": "sha512-YA2hLrwLpDsRueNDXIMqN9NTzD6bCDkuXbOSe0heS+f8YE8usA6Gbv1prj81pzVHrbaAma7zObnIC+I6/sXJgA==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/@xyflow/react": {
|
"node_modules/@xyflow/react": {
|
||||||
"version": "12.8.6",
|
"version": "12.8.6",
|
||||||
"resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.8.6.tgz",
|
"resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.8.6.tgz",
|
||||||
|
|
@ -576,6 +854,15 @@
|
||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/bidi-js": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"require-from-string": "^2.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/bl": {
|
"node_modules/bl": {
|
||||||
"version": "6.1.3",
|
"version": "6.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/bl/-/bl-6.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/bl/-/bl-6.1.3.tgz",
|
||||||
|
|
@ -674,6 +961,19 @@
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/camera-controls": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-w5oULNpijgTRH0ARFJJ0R5ct1nUM3R3WP7/b8A6j9uTGpRfnsypc/RBMPQV8JQDPayUe37p/TZZY1PcUr4czOQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.11.0",
|
||||||
|
"npm": ">=10.8.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"three": ">=0.126.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chokidar": {
|
"node_modules/chokidar": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||||
|
|
@ -740,6 +1040,45 @@
|
||||||
"node": "^14.18.0 || >=16.10.0"
|
"node": "^14.18.0 || >=16.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cross-env": {
|
||||||
|
"version": "7.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
|
||||||
|
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cross-spawn": "^7.0.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"cross-env": "src/bin/cross-env.js",
|
||||||
|
"cross-env-shell": "src/bin/cross-env-shell.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.14",
|
||||||
|
"npm": ">=6",
|
||||||
|
"yarn": ">=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cross-spawn": {
|
||||||
|
"version": "7.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
|
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"path-key": "^3.1.0",
|
||||||
|
"shebang-command": "^2.0.0",
|
||||||
|
"which": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/csstype": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/d3-color": {
|
"node_modules/d3-color": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||||
|
|
@ -932,6 +1271,15 @@
|
||||||
"integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
|
"integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/detect-gpu": {
|
||||||
|
"version": "5.0.70",
|
||||||
|
"resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.70.tgz",
|
||||||
|
"integrity": "sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"webgl-constants": "^1.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dotenv": {
|
"node_modules/dotenv": {
|
||||||
"version": "16.6.1",
|
"version": "16.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||||
|
|
@ -944,6 +1292,12 @@
|
||||||
"url": "https://dotenvx.com"
|
"url": "https://dotenvx.com"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/draco3d": {
|
||||||
|
"version": "1.5.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz",
|
||||||
|
"integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/dunder-proto": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
|
|
@ -1077,6 +1431,12 @@
|
||||||
"node": ">=8.0.0"
|
"node": ">=8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fflate": {
|
||||||
|
"version": "0.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
||||||
|
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/follow-redirects": {
|
"node_modules/follow-redirects": {
|
||||||
"version": "1.15.11",
|
"version": "1.15.11",
|
||||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||||
|
|
@ -1176,6 +1536,12 @@
|
||||||
"giget": "dist/cli.mjs"
|
"giget": "dist/cli.mjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/glsl-noise": {
|
||||||
|
"version": "0.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/glsl-noise/-/glsl-noise-0.0.0.tgz",
|
||||||
|
"integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/gopd": {
|
"node_modules/gopd": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
|
|
@ -1227,6 +1593,12 @@
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/hls.js": {
|
||||||
|
"version": "1.6.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.13.tgz",
|
||||||
|
"integrity": "sha512-hNEzjZNHf5bFrUNvdS4/1RjIanuJ6szpWNfTaX5I6WfGynWXGT7K/YQLYtemSvFExzeMdgdE4SsyVLJbd5PcZA==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/http-proxy-agent": {
|
"node_modules/http-proxy-agent": {
|
||||||
"version": "7.0.2",
|
"version": "7.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
|
||||||
|
|
@ -1285,6 +1657,12 @@
|
||||||
],
|
],
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
|
"node_modules/immediate": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||||
|
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/inherits": {
|
"node_modules/inherits": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
|
|
@ -1324,6 +1702,12 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-promise": {
|
||||||
|
"version": "2.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
|
||||||
|
"integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/is-wsl": {
|
"node_modules/is-wsl": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz",
|
||||||
|
|
@ -1339,6 +1723,33 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/isexe": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/its-fine": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/its-fine/-/its-fine-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/react-reconciler": "^0.28.9"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/its-fine/node_modules/@types/react-reconciler": {
|
||||||
|
"version": "0.28.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz",
|
||||||
|
"integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jiti": {
|
"node_modules/jiti": {
|
||||||
"version": "2.5.1",
|
"version": "2.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz",
|
||||||
|
|
@ -1397,6 +1808,15 @@
|
||||||
"safe-buffer": "^5.0.1"
|
"safe-buffer": "^5.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lie": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"immediate": "~3.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lodash.includes": {
|
"node_modules/lodash.includes": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||||
|
|
@ -1439,6 +1859,16 @@
|
||||||
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/maath": {
|
||||||
|
"version": "0.10.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/maath/-/maath-0.10.8.tgz",
|
||||||
|
"integrity": "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/three": ">=0.134.0",
|
||||||
|
"three": ">=0.134.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
|
|
@ -1448,6 +1878,21 @@
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/meshline": {
|
||||||
|
"version": "3.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/meshline/-/meshline-3.3.1.tgz",
|
||||||
|
"integrity": "sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"three": ">=0.137"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/meshoptimizer": {
|
||||||
|
"version": "0.22.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.22.0.tgz",
|
||||||
|
"integrity": "sha512-IebiK79sqIy+E4EgOr+CAw+Ke8hAspXKzBd0JdgEmPHiAwmvEj2S4h1rfvo+o/BnfEYd/jAOg5IeeIjzlzSnDg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/mime-db": {
|
"node_modules/mime-db": {
|
||||||
"version": "1.52.0",
|
"version": "1.52.0",
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
|
|
@ -1550,6 +1995,15 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/path-key": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pathe": {
|
"node_modules/pathe": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||||
|
|
@ -1650,6 +2104,12 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/potpack": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/prisma": {
|
"node_modules/prisma": {
|
||||||
"version": "6.16.2",
|
"version": "6.16.2",
|
||||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.16.2.tgz",
|
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.16.2.tgz",
|
||||||
|
|
@ -1684,6 +2144,16 @@
|
||||||
"node": ">= 0.6.0"
|
"node": ">= 0.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/promise-worker-transferable": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"is-promise": "^2.1.0",
|
||||||
|
"lie": "^3.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/proxy-from-env": {
|
"node_modules/proxy-from-env": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
|
@ -1739,6 +2209,42 @@
|
||||||
"react": "^19.2.0"
|
"react": "^19.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-reconciler": {
|
||||||
|
"version": "0.31.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.31.0.tgz",
|
||||||
|
"integrity": "sha512-7Ob7Z+URmesIsIVRjnLoDGwBEG/tVitidU0nMsqX/eeJaLY89RISO/10ERe0MqmzuKUUB1rmY+h1itMbUHg9BQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"scheduler": "^0.25.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-reconciler/node_modules/scheduler": {
|
||||||
|
"version": "0.25.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
|
||||||
|
"integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/react-use-measure": {
|
||||||
|
"version": "2.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz",
|
||||||
|
"integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.13",
|
||||||
|
"react-dom": ">=16.13"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/readable-stream": {
|
"node_modules/readable-stream": {
|
||||||
"version": "4.7.0",
|
"version": "4.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
|
||||||
|
|
@ -1768,6 +2274,15 @@
|
||||||
"url": "https://paulmillr.com/funding/"
|
"url": "https://paulmillr.com/funding/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/require-from-string": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/rfdc": {
|
"node_modules/rfdc": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
|
||||||
|
|
@ -1831,12 +2346,59 @@
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/shebang-command": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"shebang-regex": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/shebang-regex": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/sprintf-js": {
|
"node_modules/sprintf-js": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
|
||||||
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
|
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
|
"node_modules/stats-gl": {
|
||||||
|
"version": "2.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.4.2.tgz",
|
||||||
|
"integrity": "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/three": "*",
|
||||||
|
"three": "^0.170.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/three": "*",
|
||||||
|
"three": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/stats-gl/node_modules/three": {
|
||||||
|
"version": "0.170.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/three/-/three-0.170.0.tgz",
|
||||||
|
"integrity": "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/stats.js": {
|
||||||
|
"version": "0.17.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz",
|
||||||
|
"integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/string_decoder": {
|
"node_modules/string_decoder": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||||
|
|
@ -1846,6 +2408,15 @@
|
||||||
"safe-buffer": "~5.2.0"
|
"safe-buffer": "~5.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/suspend-react": {
|
||||||
|
"version": "0.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz",
|
||||||
|
"integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=17.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tarn": {
|
"node_modules/tarn": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.2.tgz",
|
||||||
|
|
@ -1876,18 +2447,95 @@
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/three": {
|
||||||
|
"version": "0.180.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/three/-/three-0.180.0.tgz",
|
||||||
|
"integrity": "sha512-o+qycAMZrh+TsE01GqWUxUIKR1AL0S8pq7zDkYOQw8GqfX8b8VoCKYUoHbhiX5j+7hr8XsuHDVU6+gkQJQKg9w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/three-mesh-bvh": {
|
||||||
|
"version": "0.8.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.8.3.tgz",
|
||||||
|
"integrity": "sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"three": ">= 0.159.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/three-stdlib": {
|
||||||
|
"version": "2.36.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.36.0.tgz",
|
||||||
|
"integrity": "sha512-kv0Byb++AXztEGsULgMAs8U2jgUdz6HPpAB/wDJnLiLlaWQX2APHhiTJIN7rqW+Of0eRgcp7jn05U1BsCP3xBA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/draco3d": "^1.4.0",
|
||||||
|
"@types/offscreencanvas": "^2019.6.4",
|
||||||
|
"@types/webxr": "^0.5.2",
|
||||||
|
"draco3d": "^1.4.1",
|
||||||
|
"fflate": "^0.6.9",
|
||||||
|
"potpack": "^1.0.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"three": ">=0.128.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/three-stdlib/node_modules/fflate": {
|
||||||
|
"version": "0.6.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz",
|
||||||
|
"integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tinyexec": {
|
"node_modules/tinyexec": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz",
|
||||||
"integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==",
|
"integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/troika-three-text": {
|
||||||
|
"version": "0.52.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.4.tgz",
|
||||||
|
"integrity": "sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bidi-js": "^1.0.2",
|
||||||
|
"troika-three-utils": "^0.52.4",
|
||||||
|
"troika-worker-utils": "^0.52.0",
|
||||||
|
"webgl-sdf-generator": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"three": ">=0.125.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/troika-three-utils": {
|
||||||
|
"version": "0.52.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.52.4.tgz",
|
||||||
|
"integrity": "sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"three": ">=0.125.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/troika-worker-utils": {
|
||||||
|
"version": "0.52.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.52.0.tgz",
|
||||||
|
"integrity": "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tslib": {
|
"node_modules/tslib": {
|
||||||
"version": "2.8.1",
|
"version": "2.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
|
"node_modules/tunnel-rat": {
|
||||||
|
"version": "0.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/tunnel-rat/-/tunnel-rat-0.1.2.tgz",
|
||||||
|
"integrity": "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"zustand": "^4.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "7.12.0",
|
"version": "7.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz",
|
||||||
|
|
@ -1903,6 +2551,15 @@
|
||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/utility-types": {
|
||||||
|
"version": "3.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz",
|
||||||
|
"integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/uuid": {
|
"node_modules/uuid": {
|
||||||
"version": "8.3.2",
|
"version": "8.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||||
|
|
@ -1912,6 +2569,32 @@
|
||||||
"uuid": "dist/bin/uuid"
|
"uuid": "dist/bin/uuid"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/webgl-constants": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg=="
|
||||||
|
},
|
||||||
|
"node_modules/webgl-sdf-generator": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/which": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"isexe": "^2.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"node-which": "bin/node-which"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/wsl-utils": {
|
"node_modules/wsl-utils": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^6.16.2",
|
"@prisma/client": "^6.16.2",
|
||||||
|
"@react-three/drei": "^10.7.6",
|
||||||
|
"@react-three/fiber": "^9.4.0",
|
||||||
"@types/mssql": "^9.1.8",
|
"@types/mssql": "^9.1.8",
|
||||||
"@xyflow/react": "^12.8.6",
|
"@xyflow/react": "^12.8.6",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
"mssql": "^11.0.1",
|
"mssql": "^11.0.1",
|
||||||
"prisma": "^6.16.2"
|
"prisma": "^6.16.2",
|
||||||
|
"three": "^0.180.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/oracledb": "^6.9.1",
|
"@types/oracledb": "^6.9.1",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue