350 lines
13 KiB
TypeScript
350 lines
13 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState } from "react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import {
|
|
Group,
|
|
Ungroup,
|
|
Palette,
|
|
Settings,
|
|
X,
|
|
Check,
|
|
AlignLeft,
|
|
AlignCenter,
|
|
AlignRight,
|
|
StretchHorizontal,
|
|
StretchVertical,
|
|
} from "lucide-react";
|
|
import { GroupState, ComponentData, ComponentStyle } from "@/types/screen";
|
|
import { createGroupStyle } from "@/lib/utils/groupingUtils";
|
|
|
|
interface GroupingToolbarProps {
|
|
groupState: GroupState;
|
|
onGroupStateChange: (state: GroupState) => void;
|
|
onGroupCreate: (componentIds: string[], title: string, style?: ComponentStyle) => void;
|
|
onGroupUngroup: (groupId: string) => void;
|
|
selectedComponents: ComponentData[];
|
|
allComponents: ComponentData[];
|
|
onGroupAlign?: (mode: "left" | "centerX" | "right" | "top" | "centerY" | "bottom") => void;
|
|
onGroupDistribute?: (orientation: "horizontal" | "vertical") => void;
|
|
}
|
|
|
|
export const GroupingToolbar: React.FC<GroupingToolbarProps> = ({
|
|
groupState,
|
|
onGroupStateChange,
|
|
onGroupCreate,
|
|
onGroupUngroup,
|
|
selectedComponents,
|
|
allComponents,
|
|
onGroupAlign,
|
|
onGroupDistribute,
|
|
}) => {
|
|
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
|
const [groupTitle, setGroupTitle] = useState("새 그룹");
|
|
const [groupStyle, setGroupStyle] = useState<ComponentStyle>(createGroupStyle());
|
|
|
|
// 선택된 컴포넌트가 2개 이상인지 확인
|
|
const canCreateGroup = selectedComponents.length >= 2;
|
|
|
|
// 선택된 컴포넌트가 그룹인지 확인
|
|
const selectedGroup = selectedComponents.length === 1 && selectedComponents[0].type === "group";
|
|
|
|
const handleCreateGroup = () => {
|
|
if (canCreateGroup) {
|
|
setGroupTitle("새 그룹");
|
|
setGroupStyle(createGroupStyle());
|
|
setShowCreateDialog(true);
|
|
}
|
|
};
|
|
|
|
const handleUngroup = () => {
|
|
if (selectedGroup) {
|
|
onGroupUngroup(selectedComponents[0].id);
|
|
onGroupStateChange({
|
|
...groupState,
|
|
selectedComponents: [],
|
|
isGrouping: false,
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleConfirmCreate = () => {
|
|
if (groupTitle.trim()) {
|
|
const componentIds = selectedComponents.map((c) => c.id);
|
|
onGroupCreate(componentIds, groupTitle.trim(), groupStyle);
|
|
setShowCreateDialog(false);
|
|
onGroupStateChange({
|
|
...groupState,
|
|
selectedComponents: [],
|
|
isGrouping: false,
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleCancelCreate = () => {
|
|
setShowCreateDialog(false);
|
|
setGroupTitle("새 그룹");
|
|
setGroupStyle(createGroupStyle());
|
|
};
|
|
|
|
const handleStyleChange = (property: string, value: string) => {
|
|
setGroupStyle((prev) => ({
|
|
...prev,
|
|
[property]: value,
|
|
}));
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div className="flex items-center space-x-2 border-b bg-gray-50 p-2">
|
|
<div className="flex items-center space-x-2">
|
|
<Group className="h-4 w-4 text-blue-600" />
|
|
<span className="text-sm font-medium">그룹화</span>
|
|
</div>
|
|
|
|
{/* 선택된 컴포넌트 표시 */}
|
|
{selectedComponents.length > 0 && (
|
|
<Badge variant="secondary" className="ml-2">
|
|
{selectedComponents.length}개 선택됨
|
|
{selectedComponents.length > 1 && (
|
|
<span className="ml-1 text-xs opacity-75">(Shift+클릭으로 다중선택, 드래그로 함께 이동)</span>
|
|
)}
|
|
</Badge>
|
|
)}
|
|
|
|
<div className="ml-auto flex items-center space-x-1">
|
|
{/* 그룹 생성 버튼 */}
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={handleCreateGroup}
|
|
disabled={!canCreateGroup}
|
|
title={canCreateGroup ? "선택된 컴포넌트를 그룹으로 묶기" : "2개 이상의 컴포넌트를 선택하세요"}
|
|
>
|
|
<Group className="mr-1 h-3 w-3" />
|
|
그룹 생성
|
|
</Button>
|
|
|
|
{/* 그룹 해제 버튼 */}
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={handleUngroup}
|
|
disabled={!selectedGroup}
|
|
title={selectedGroup ? "선택된 그룹 해제" : "그룹을 선택하세요"}
|
|
>
|
|
<Ungroup className="mr-1 h-3 w-3" />
|
|
그룹 해제
|
|
</Button>
|
|
|
|
{/* 선택 해제 버튼 */}
|
|
{selectedComponents.length > 0 && (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() =>
|
|
onGroupStateChange({
|
|
...groupState,
|
|
selectedComponents: [],
|
|
isGrouping: false,
|
|
})
|
|
}
|
|
title="선택 해제"
|
|
>
|
|
<X className="h-3 w-3" />
|
|
</Button>
|
|
)}
|
|
|
|
{/* 정렬/분배 도구 */}
|
|
{selectedComponents.length > 1 && (
|
|
<div className="ml-2 flex items-center space-x-1">
|
|
<span className="mr-1 text-xs text-gray-500">정렬</span>
|
|
<Button variant="outline" size="sm" onClick={() => onGroupAlign?.("left")} title="좌측 정렬">
|
|
<AlignLeft className="h-3 w-3" />
|
|
</Button>
|
|
<Button variant="outline" size="sm" onClick={() => onGroupAlign?.("centerX")} title="가로 중앙 정렬">
|
|
<AlignCenter className="h-3 w-3" />
|
|
</Button>
|
|
<Button variant="outline" size="sm" onClick={() => onGroupAlign?.("right")} title="우측 정렬">
|
|
<AlignRight className="h-3 w-3" />
|
|
</Button>
|
|
<Button variant="outline" size="sm" onClick={() => onGroupAlign?.("top")} title="상단 정렬">
|
|
<AlignLeft className="h-3 w-3 rotate-90" />
|
|
</Button>
|
|
<Button variant="outline" size="sm" onClick={() => onGroupAlign?.("centerY")} title="세로 중앙 정렬">
|
|
<AlignCenter className="h-3 w-3 rotate-90" />
|
|
</Button>
|
|
<Button variant="outline" size="sm" onClick={() => onGroupAlign?.("bottom")} title="하단 정렬">
|
|
<AlignRight className="h-3 w-3 rotate-90" />
|
|
</Button>
|
|
<div className="mx-1 h-4 w-px bg-gray-200" />
|
|
<span className="mr-1 text-xs text-gray-500">균등</span>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => onGroupDistribute?.("horizontal")}
|
|
title="가로 균등 분배"
|
|
>
|
|
<StretchHorizontal className="h-3 w-3" />
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => onGroupDistribute?.("vertical")}
|
|
title="세로 균등 분배"
|
|
>
|
|
<StretchVertical className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 그룹 생성 다이얼로그 */}
|
|
<Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}>
|
|
<DialogContent className="sm:max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle>그룹 생성</DialogTitle>
|
|
<DialogDescription>선택된 {selectedComponents.length}개의 컴포넌트를 그룹으로 묶습니다.</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4">
|
|
{/* 그룹 제목 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="groupTitle">그룹 제목</Label>
|
|
<Input
|
|
id="groupTitle"
|
|
value={groupTitle}
|
|
onChange={(e) => setGroupTitle(e.target.value)}
|
|
placeholder="그룹 제목을 입력하세요"
|
|
maxLength={50}
|
|
/>
|
|
</div>
|
|
|
|
{/* 그룹 스타일 */}
|
|
<div className="space-y-4">
|
|
<div className="flex items-center space-x-2">
|
|
<Palette className="h-4 w-4 text-gray-600" />
|
|
<Label className="text-sm font-medium">그룹 스타일</Label>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
{/* 배경색 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="backgroundColor" className="text-xs">
|
|
배경색
|
|
</Label>
|
|
<Select
|
|
value={groupStyle.backgroundColor || "#f8f9fa"}
|
|
onValueChange={(value) => handleStyleChange("backgroundColor", value)}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="#f8f9fa">연한 회색</SelectItem>
|
|
<SelectItem value="#ffffff">흰색</SelectItem>
|
|
<SelectItem value="#e3f2fd">연한 파란색</SelectItem>
|
|
<SelectItem value="#f3e5f5">연한 보라색</SelectItem>
|
|
<SelectItem value="#e8f5e8">연한 초록색</SelectItem>
|
|
<SelectItem value="#fff3e0">연한 주황색</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 테두리 스타일 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="borderStyle" className="text-xs">
|
|
테두리
|
|
</Label>
|
|
<Select
|
|
value={groupStyle.borderStyle || "solid"}
|
|
onValueChange={(value) => handleStyleChange("borderStyle", value)}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="solid">실선</SelectItem>
|
|
<SelectItem value="dashed">점선</SelectItem>
|
|
<SelectItem value="dotted">점</SelectItem>
|
|
<SelectItem value="none">없음</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 테두리 색상 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="borderColor" className="text-xs">
|
|
테두리 색상
|
|
</Label>
|
|
<Select
|
|
value={groupStyle.borderColor || "#dee2e6"}
|
|
onValueChange={(value) => handleStyleChange("borderColor", value)}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="#dee2e6">회색</SelectItem>
|
|
<SelectItem value="#007bff">파란색</SelectItem>
|
|
<SelectItem value="#28a745">초록색</SelectItem>
|
|
<SelectItem value="#ffc107">노란색</SelectItem>
|
|
<SelectItem value="#dc3545">빨간색</SelectItem>
|
|
<SelectItem value="#6f42c1">보라색</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 모서리 둥글기 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="borderRadius" className="text-xs">
|
|
모서리
|
|
</Label>
|
|
<Select
|
|
value={String(groupStyle.borderRadius || 8)}
|
|
onValueChange={(value) => handleStyleChange("borderRadius", value)}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="0">각진 모서리</SelectItem>
|
|
<SelectItem value="4">약간 둥근 모서리</SelectItem>
|
|
<SelectItem value="8">둥근 모서리</SelectItem>
|
|
<SelectItem value="12">매우 둥근 모서리</SelectItem>
|
|
<SelectItem value="16">완전히 둥근 모서리</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={handleCancelCreate}>
|
|
취소
|
|
</Button>
|
|
<Button onClick={handleConfirmCreate} disabled={!groupTitle.trim()}>
|
|
<Check className="mr-1 h-3 w-3" />
|
|
그룹 생성
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</>
|
|
);
|
|
};
|