This commit is contained in:
kjs 2025-10-17 16:21:08 +09:00
parent 2a8081a253
commit 2e916678fa
22 changed files with 1641 additions and 871 deletions

View File

@ -114,11 +114,11 @@ export const FloatingPanel: React.FC<FloatingPanelProps> = ({
const newHeight = Math.min(Math.max(minHeight, contentHeight + headerHeight + padding), maxHeight); const newHeight = Math.min(Math.max(minHeight, contentHeight + headerHeight + padding), maxHeight);
// console.log(`🔧 패널 높이 자동 조정:`, { // console.log(`🔧 패널 높이 자동 조정:`, {
// panelId: id, // panelId: id,
// contentHeight, // contentHeight,
// calculatedHeight: newHeight, // calculatedHeight: newHeight,
// currentHeight: panelSize.height, // currentHeight: panelSize.height,
// willUpdate: Math.abs(panelSize.height - newHeight) > 10, // willUpdate: Math.abs(panelSize.height - newHeight) > 10,
// }); // });
// 현재 높이와 다르면 업데이트 // 현재 높이와 다르면 업데이트
@ -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>

View File

@ -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>

View File

@ -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>
<Input </Label>
id="gap" <Input
type="text" id="gap"
placeholder="10px, 1rem" type="text"
value={localStyle.gap || ""} placeholder="10px"
onChange={(e) => handleStyleChange("gap", e.target.value)} value={localStyle.gap || ""}
/> onChange={(e) => handleStyleChange("gap", e.target.value)}
</div> 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">
<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">
<Input
id="borderColor" </Label>
type="color" <div className="flex gap-2">
value={localStyle.borderColor || "#000000"} <Input
onChange={(e) => handleStyleChange("borderColor", e.target.value)} id="borderColor"
/> type="color"
value={localStyle.borderColor || "#000000"}
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>
<div className="space-y-2"> <div className="space-y-1.5">
<Label htmlFor="borderRadius"> </Label> <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">
<Input
id="backgroundColor" </Label>
type="color" <div className="flex gap-2">
value={localStyle.backgroundColor || "#ffffff"} <Input
onChange={(e) => handleStyleChange("backgroundColor", e.target.value)} id="backgroundColor"
/> type="color"
value={localStyle.backgroundColor || "#ffffff"}
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">
<Input
id="color" </Label>
type="color" <div className="flex gap-2">
value={localStyle.color || "#000000"} <Input
onChange={(e) => handleStyleChange("color", e.target.value)} id="color"
/> type="color"
value={localStyle.color || "#000000"}
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>
<div className="space-y-2"> <div className="space-y-1.5">
<Label htmlFor="fontSize"> </Label> <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>

View File

@ -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,14 +209,12 @@ 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>

View File

@ -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>

View File

@ -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 && (

View File

@ -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>

View File

@ -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>
); );

View File

@ -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>
); );

View File

@ -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>
))} ))}

View File

@ -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>
); );

View File

@ -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>
); );
} }

View File

@ -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 : {}),
}} }}

View File

@ -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>
); );
}; };

View File

@ -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
`, `,
}; };

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

685
package-lock.json generated
View File

@ -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",

View File

@ -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",