607 lines
23 KiB
TypeScript
607 lines
23 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* ComponentStylePanel.tsx — 우측 패널 전용 디자인 설정 컨테이너
|
|
*
|
|
* 컴포넌트 타입별 전용 디자인 아코디언이 최상단에 동적 생성되고,
|
|
* 공통 속성(배치/글꼴/배경/테두리)이 그 아래에 위치한다.
|
|
*/
|
|
|
|
import { useState } from "react";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import { useReportDesigner } from "@/contexts/ReportDesignerContext";
|
|
import {
|
|
ChevronRight,
|
|
Move,
|
|
Type as TypeIcon,
|
|
Square,
|
|
CreditCard,
|
|
Table2,
|
|
ImageIcon,
|
|
Minus,
|
|
PenTool,
|
|
Stamp,
|
|
Hash,
|
|
QrCode,
|
|
CheckSquare,
|
|
Tag,
|
|
BarChart3,
|
|
Lock,
|
|
Unlock,
|
|
Layers,
|
|
ArrowUpToLine,
|
|
ArrowDownToLine,
|
|
ArrowUp,
|
|
ArrowDown,
|
|
Group,
|
|
Ungroup,
|
|
} from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import type { ComponentConfig } from "@/types/report";
|
|
|
|
import { CardProperties } from "./CardProperties";
|
|
import { DividerProperties } from "./DividerProperties";
|
|
import { ImageProperties } from "./ImageProperties";
|
|
import { TableProperties } from "./TableProperties";
|
|
import { TextProperties } from "./TextProperties";
|
|
|
|
const TYPE_LABELS: Record<string, string> = {
|
|
text: "텍스트",
|
|
label: "레이블",
|
|
table: "테이블",
|
|
image: "이미지",
|
|
divider: "구분선",
|
|
signature: "서명",
|
|
stamp: "도장",
|
|
pageNumber: "페이지 번호",
|
|
card: "카드",
|
|
calculation: "계산",
|
|
barcode: "바코드",
|
|
checkbox: "체크박스",
|
|
};
|
|
|
|
const TYPE_ICONS: Record<string, React.ReactNode> = {
|
|
text: <TypeIcon className="h-4 w-4" />,
|
|
label: <Tag className="h-4 w-4" />,
|
|
table: <Table2 className="h-4 w-4" />,
|
|
image: <ImageIcon className="h-4 w-4" />,
|
|
divider: <Minus className="h-4 w-4" />,
|
|
signature: <PenTool className="h-4 w-4" />,
|
|
stamp: <Stamp className="h-4 w-4" />,
|
|
pageNumber: <Hash className="h-4 w-4" />,
|
|
card: <CreditCard className="h-4 w-4" />,
|
|
calculation: <BarChart3 className="h-4 w-4" />,
|
|
barcode: <QrCode className="h-4 w-4" />,
|
|
checkbox: <CheckSquare className="h-4 w-4" />,
|
|
};
|
|
|
|
interface AccordionSectionProps {
|
|
id: string;
|
|
label: string;
|
|
icon: React.ReactNode;
|
|
isExpanded: boolean;
|
|
onToggle: () => void;
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
function AccordionSection({ label, icon, isExpanded, onToggle, children }: AccordionSectionProps) {
|
|
return (
|
|
<div className="border-b border-gray-100">
|
|
<button
|
|
onClick={onToggle}
|
|
className={`flex h-11 w-full items-center justify-between px-4 transition-colors ${
|
|
isExpanded
|
|
? "bg-linear-to-r from-blue-600 to-indigo-600 text-white shadow-sm"
|
|
: "bg-white text-gray-900 hover:bg-gray-50"
|
|
}`}
|
|
>
|
|
<div className="flex items-center gap-2.5">
|
|
<span className={isExpanded ? "" : "text-blue-600"}>{icon}</span>
|
|
<span className="text-sm font-bold">{label}</span>
|
|
</div>
|
|
<ChevronRight
|
|
className={`h-4 w-4 transition-transform ${isExpanded ? "rotate-90" : "text-gray-400"}`}
|
|
/>
|
|
</button>
|
|
{isExpanded && (
|
|
<div className="border-t border-blue-100 bg-linear-to-b from-blue-50/30 to-white p-4">
|
|
{children}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function TypeSpecificDesign({
|
|
component,
|
|
update,
|
|
}: {
|
|
component: ComponentConfig;
|
|
update: (changes: Partial<ComponentConfig>) => void;
|
|
}) {
|
|
const type = component.type;
|
|
|
|
if (type === "text" || type === "label") {
|
|
return (
|
|
<TextProperties component={component} section="style" />
|
|
);
|
|
}
|
|
|
|
if (type === "table") {
|
|
return (
|
|
<TableProperties component={component} section="style" />
|
|
);
|
|
}
|
|
|
|
if (type === "image") {
|
|
return (
|
|
<ImageProperties component={component} section="style" />
|
|
);
|
|
}
|
|
|
|
if (type === "divider") {
|
|
return (
|
|
<DividerProperties component={component} section="style" />
|
|
);
|
|
}
|
|
|
|
if (type === "signature" || type === "stamp") {
|
|
return (
|
|
<div className="space-y-3">
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<Label className="text-xs">표시 방식</Label>
|
|
<Select value={component.objectFit || "contain"} onValueChange={(v) => update({ objectFit: v as ComponentConfig["objectFit"] })}>
|
|
<SelectTrigger className="h-9 text-sm"><SelectValue /></SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="contain">비율 유지</SelectItem>
|
|
<SelectItem value="cover">채우기</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs">라벨 위치</Label>
|
|
<Select value={component.labelPosition || "left"} onValueChange={(v) => update({ labelPosition: v as ComponentConfig["labelPosition"] })}>
|
|
<SelectTrigger className="h-9 text-sm"><SelectValue /></SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="left">왼쪽</SelectItem>
|
|
<SelectItem value="right">오른쪽</SelectItem>
|
|
<SelectItem value="top">위</SelectItem>
|
|
<SelectItem value="bottom">아래</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<input type="checkbox" checked={component.showLabel !== false} onChange={(e) => update({ showLabel: e.target.checked })} className="h-4 w-4 rounded" />
|
|
<Label className="text-xs">라벨 표시</Label>
|
|
</div>
|
|
{component.showLabel !== false && (
|
|
<div>
|
|
<Label className="text-xs">라벨 텍스트</Label>
|
|
<Input value={component.labelText || (type === "stamp" ? "(인)" : "서명:")} onChange={(e) => update({ labelText: e.target.value })} className="h-9 text-sm" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (type === "pageNumber") {
|
|
return (
|
|
<div className="space-y-3">
|
|
<div>
|
|
<Label className="text-xs">페이지 번호 형식</Label>
|
|
<Select value={component.pageNumberFormat || "number"} onValueChange={(v) => update({ pageNumberFormat: v as ComponentConfig["pageNumberFormat"] })}>
|
|
<SelectTrigger className="h-9 text-sm"><SelectValue /></SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="number">1</SelectItem>
|
|
<SelectItem value="numberTotal">1 / 3</SelectItem>
|
|
<SelectItem value="koreanNumber">1 페이지</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<Label className="text-xs">텍스트 정렬</Label>
|
|
<Select value={component.textAlign || "center"} onValueChange={(v) => update({ textAlign: v })}>
|
|
<SelectTrigger className="h-9 text-sm"><SelectValue /></SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="left">왼쪽</SelectItem>
|
|
<SelectItem value="center">가운데</SelectItem>
|
|
<SelectItem value="right">오른쪽</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs">글꼴 굵기</Label>
|
|
<Select value={component.fontWeight || "normal"} onValueChange={(v) => update({ fontWeight: v })}>
|
|
<SelectTrigger className="h-9 text-sm"><SelectValue /></SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="normal">보통</SelectItem>
|
|
<SelectItem value="bold">굵게</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (type === "calculation") {
|
|
return (
|
|
<div className="space-y-3">
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<Label className="text-xs">숫자 형식</Label>
|
|
<Select value={component.numberFormat || "currency"} onValueChange={(v) => update({ numberFormat: v as ComponentConfig["numberFormat"] })}>
|
|
<SelectTrigger className="h-9 text-sm"><SelectValue /></SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="none">없음</SelectItem>
|
|
<SelectItem value="comma">콤마 (1,000)</SelectItem>
|
|
<SelectItem value="currency">통화</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
{component.numberFormat === "currency" && (
|
|
<div>
|
|
<Label className="text-xs">통화 단위</Label>
|
|
<Input value={component.currencySuffix || "원"} onChange={(e) => update({ currencySuffix: e.target.value })} className="h-9 text-sm" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<Label className="text-xs">결과 색상</Label>
|
|
<div className="flex items-center gap-2">
|
|
<input type="color" value={component.resultColor || "#2563eb"} onChange={(e) => update({ resultColor: e.target.value })} className="w-9 h-9 rounded cursor-pointer border border-gray-200" />
|
|
<Input value={component.resultColor || "#2563eb"} onChange={(e) => update({ resultColor: e.target.value })} className="h-9 text-sm flex-1" />
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs">결과 크기</Label>
|
|
<Input type="number" min={10} max={30} value={component.resultFontSize || 16} onChange={(e) => update({ resultFontSize: parseInt(e.target.value) || 16 })} className="h-9 text-sm" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (type === "barcode") {
|
|
return (
|
|
<div className="space-y-3">
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<Label className="text-xs">바코드 색상</Label>
|
|
<div className="flex items-center gap-2">
|
|
<input type="color" value={component.barcodeColor || "#000000"} onChange={(e) => update({ barcodeColor: e.target.value })} className="w-9 h-9 rounded cursor-pointer border border-gray-200" />
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs">배경 색상</Label>
|
|
<div className="flex items-center gap-2">
|
|
<input type="color" value={component.barcodeBackground || "#ffffff"} onChange={(e) => update({ barcodeBackground: e.target.value })} className="w-9 h-9 rounded cursor-pointer border border-gray-200" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<input type="checkbox" checked={component.showBarcodeText !== false} onChange={(e) => update({ showBarcodeText: e.target.checked })} className="h-4 w-4 rounded" />
|
|
<Label className="text-xs">하단 텍스트 표시</Label>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs">여백 (px)</Label>
|
|
<Input type="number" min={0} max={30} value={component.barcodeMargin ?? 10} onChange={(e) => update({ barcodeMargin: parseInt(e.target.value) || 0 })} className="h-9 text-sm" />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (type === "checkbox") {
|
|
return (
|
|
<div className="space-y-3">
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<Label className="text-xs">체크 색상</Label>
|
|
<div className="flex items-center gap-2">
|
|
<input type="color" value={component.checkboxColor || "#2563eb"} onChange={(e) => update({ checkboxColor: e.target.value })} className="w-9 h-9 rounded cursor-pointer border border-gray-200" />
|
|
<Input value={component.checkboxColor || "#2563eb"} onChange={(e) => update({ checkboxColor: e.target.value })} className="h-9 text-sm flex-1" />
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs">테두리 색상</Label>
|
|
<div className="flex items-center gap-2">
|
|
<input type="color" value={component.checkboxBorderColor || "#6b7280"} onChange={(e) => update({ checkboxBorderColor: e.target.value })} className="w-9 h-9 rounded cursor-pointer border border-gray-200" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<Label className="text-xs">크기 (px)</Label>
|
|
<Input type="number" min={12} max={40} value={component.checkboxSize || 18} onChange={(e) => update({ checkboxSize: parseInt(e.target.value) || 18 })} className="h-9 text-sm" />
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs">라벨 위치</Label>
|
|
<Select value={component.checkboxLabelPosition || "right"} onValueChange={(v) => update({ checkboxLabelPosition: v as "left" | "right" })}>
|
|
<SelectTrigger className="h-9 text-sm"><SelectValue /></SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="left">왼쪽</SelectItem>
|
|
<SelectItem value="right">오른쪽</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (type === "statusBadge") {
|
|
return (
|
|
<div className="space-y-3">
|
|
<p className="text-xs text-muted-foreground">상태 매핑은 모달(더블클릭)에서 설정합니다.</p>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<Label className="text-xs">텍스트 정렬</Label>
|
|
<Select value={component.textAlign || "center"} onValueChange={(v) => update({ textAlign: v })}>
|
|
<SelectTrigger className="h-9 text-sm"><SelectValue /></SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="left">왼쪽</SelectItem>
|
|
<SelectItem value="center">가운데</SelectItem>
|
|
<SelectItem value="right">오른쪽</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs">글꼴 굵기</Label>
|
|
<Select value={component.fontWeight || "bold"} onValueChange={(v) => update({ fontWeight: v })}>
|
|
<SelectTrigger className="h-9 text-sm"><SelectValue /></SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="normal">보통</SelectItem>
|
|
<SelectItem value="bold">굵게</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export function ComponentStylePanel() {
|
|
const {
|
|
selectedComponentId,
|
|
selectedComponentIds,
|
|
components,
|
|
updateComponent,
|
|
bringToFront,
|
|
sendToBack,
|
|
bringForward,
|
|
sendBackward,
|
|
toggleLock,
|
|
groupComponents,
|
|
ungroupComponents,
|
|
} = useReportDesigner();
|
|
|
|
const [expandedSections, setExpandedSections] = useState<Set<string>>(new Set(["typeDesign"]));
|
|
|
|
const component =
|
|
components.find((c) => c.id === selectedComponentId) ?? null;
|
|
|
|
if (!component) return null;
|
|
|
|
const typeLabel = TYPE_LABELS[component.type] ?? component.type;
|
|
const update = (changes: Partial<ComponentConfig>) =>
|
|
updateComponent(component.id, changes);
|
|
|
|
const toggleSection = (id: string) => {
|
|
setExpandedSections((prev) => {
|
|
const next = new Set(prev);
|
|
if (next.has(id)) next.delete(id);
|
|
else next.add(id);
|
|
return next;
|
|
});
|
|
};
|
|
|
|
const hasTypeDesign = component.type !== "card";
|
|
const typeIcon = TYPE_ICONS[component.type] || <Square className="h-4 w-4" />;
|
|
|
|
return (
|
|
<div className="flex h-full flex-col overflow-hidden">
|
|
<div className="flex-1 overflow-y-auto">
|
|
{/* 카드 전용: CardProperties 하위 아코디언 포함 */}
|
|
{component.type === "card" && (
|
|
<AccordionSection
|
|
id="cardDesign"
|
|
label="카드 디자인"
|
|
icon={<CreditCard className="h-4 w-4" />}
|
|
isExpanded={expandedSections.has("cardDesign")}
|
|
onToggle={() => toggleSection("cardDesign")}
|
|
>
|
|
<CardProperties component={component} section="style" />
|
|
</AccordionSection>
|
|
)}
|
|
|
|
{/* 카드 외 모든 타입: 타입별 전용 디자인 */}
|
|
{hasTypeDesign && (
|
|
<AccordionSection
|
|
id="typeDesign"
|
|
label={`${typeLabel} 디자인`}
|
|
icon={typeIcon}
|
|
isExpanded={expandedSections.has("typeDesign")}
|
|
onToggle={() => toggleSection("typeDesign")}
|
|
>
|
|
<TypeSpecificDesign component={component} update={update} />
|
|
</AccordionSection>
|
|
)}
|
|
|
|
{/* 배치 (X/Y/W/H) */}
|
|
<AccordionSection
|
|
id="layout"
|
|
label="배치"
|
|
icon={<Move className="h-4 w-4" />}
|
|
isExpanded={expandedSections.has("layout")}
|
|
onToggle={() => toggleSection("layout")}
|
|
>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{([
|
|
{ label: "X", key: "x" as const, min: undefined },
|
|
{ label: "Y", key: "y" as const, min: undefined },
|
|
{ label: "너비", key: "width" as const, min: 50 },
|
|
{ label: "높이", key: "height" as const, min: 30 },
|
|
] as const).map(({ label, key, min }) => (
|
|
<div key={key}>
|
|
<Label className="text-xs">{label}</Label>
|
|
<Input
|
|
type="number"
|
|
value={Math.round(component[key] as number)}
|
|
onChange={(e) =>
|
|
update({ [key]: parseInt(e.target.value) || min || 0 })
|
|
}
|
|
className="h-9 text-sm"
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</AccordionSection>
|
|
|
|
{/* 잠금 */}
|
|
<AccordionSection
|
|
id="lock"
|
|
label="잠금"
|
|
icon={<Lock className="h-4 w-4" />}
|
|
isExpanded={expandedSections.has("lock")}
|
|
onToggle={() => toggleSection("lock")}
|
|
>
|
|
<div className="space-y-2">
|
|
<Button
|
|
variant={component.locked ? "destructive" : "outline"}
|
|
size="sm"
|
|
className="w-full h-9 gap-2 text-sm"
|
|
onClick={() => toggleLock()}
|
|
>
|
|
{component.locked ? (
|
|
<>
|
|
<Lock className="h-3.5 w-3.5" />
|
|
잠금 해제
|
|
</>
|
|
) : (
|
|
<>
|
|
<Unlock className="h-3.5 w-3.5" />
|
|
위치 잠금
|
|
</>
|
|
)}
|
|
</Button>
|
|
<p className="text-[10px] text-muted-foreground">
|
|
잠금 시 드래그/리사이즈 불가
|
|
</p>
|
|
</div>
|
|
</AccordionSection>
|
|
|
|
{/* 레이어 */}
|
|
<AccordionSection
|
|
id="layer"
|
|
label="레이어"
|
|
icon={<Layers className="h-4 w-4" />}
|
|
isExpanded={expandedSections.has("layer")}
|
|
onToggle={() => toggleSection("layer")}
|
|
>
|
|
<div className="grid grid-cols-2 gap-1.5">
|
|
<Button variant="outline" size="sm" className="h-8 gap-1 text-xs" onClick={() => bringToFront()} title="맨 앞으로">
|
|
<ArrowUpToLine className="h-3.5 w-3.5" />
|
|
맨 앞
|
|
</Button>
|
|
<Button variant="outline" size="sm" className="h-8 gap-1 text-xs" onClick={() => sendToBack()} title="맨 뒤로">
|
|
<ArrowDownToLine className="h-3.5 w-3.5" />
|
|
맨 뒤
|
|
</Button>
|
|
<Button variant="outline" size="sm" className="h-8 gap-1 text-xs" onClick={() => bringForward()} title="한 단계 앞으로">
|
|
<ArrowUp className="h-3.5 w-3.5" />
|
|
앞으로
|
|
</Button>
|
|
<Button variant="outline" size="sm" className="h-8 gap-1 text-xs" onClick={() => sendBackward()} title="한 단계 뒤로">
|
|
<ArrowDown className="h-3.5 w-3.5" />
|
|
뒤로
|
|
</Button>
|
|
</div>
|
|
</AccordionSection>
|
|
|
|
{/* 그룹 */}
|
|
<AccordionSection
|
|
id="group"
|
|
label={`그룹${component.groupId ? " (그룹됨)" : ""}`}
|
|
icon={<Group className="h-4 w-4" />}
|
|
isExpanded={expandedSections.has("group")}
|
|
onToggle={() => toggleSection("group")}
|
|
>
|
|
{component.groupId ? (
|
|
<div className="space-y-2.5">
|
|
<div className="rounded-md border border-purple-200 bg-purple-50 px-3 py-2">
|
|
<p className="text-xs text-purple-700">
|
|
그룹 내 컴포넌트를 드래그하면 함께 이동합니다.
|
|
</p>
|
|
<p className="text-[10px] text-purple-500 mt-1">
|
|
같은 그룹: {components.filter((c) => c.groupId === component.groupId).length}개
|
|
</p>
|
|
</div>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="w-full h-8 gap-1.5 text-xs border-purple-300 text-purple-700 hover:bg-purple-50"
|
|
onClick={() => ungroupComponents()}
|
|
>
|
|
<Ungroup className="h-3.5 w-3.5" />
|
|
그룹 해제
|
|
</Button>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="w-full h-8 gap-1.5 text-xs"
|
|
onClick={() => groupComponents()}
|
|
disabled={selectedComponentIds.length < 2}
|
|
>
|
|
<Group className="h-3.5 w-3.5" />
|
|
선택 컴포넌트 그룹화
|
|
</Button>
|
|
<p className="text-[10px] text-muted-foreground">
|
|
{selectedComponentIds.length < 2
|
|
? "드래그 또는 Ctrl+클릭으로 2개 이상 선택 후 그룹화"
|
|
: `${selectedComponentIds.length}개 선택됨 - 그룹화 가능`}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</AccordionSection>
|
|
|
|
{/* 테두리 */}
|
|
<AccordionSection
|
|
id="border"
|
|
label="테두리"
|
|
icon={<Square className="h-4 w-4" />}
|
|
isExpanded={expandedSections.has("border")}
|
|
onToggle={() => toggleSection("border")}
|
|
>
|
|
<div>
|
|
<Label className="text-xs">모양</Label>
|
|
<div className="flex gap-2 mt-1">
|
|
<button onClick={() => update({ borderRadius: 0 })} className={`flex-1 h-9 rounded-md border text-xs font-medium transition-colors ${!component.borderRadius || component.borderRadius === 0 ? "border-blue-500 bg-blue-50 text-blue-700" : "border-gray-200 bg-white text-gray-600 hover:bg-gray-50"}`}>사각형</button>
|
|
<button onClick={() => update({ borderRadius: 16 })} className={`flex-1 h-9 rounded-md border text-xs font-medium transition-colors ${component.borderRadius && component.borderRadius > 0 ? "border-blue-500 bg-blue-50 text-blue-700" : "border-gray-200 bg-white text-gray-600 hover:bg-gray-50"}`}>둥글게</button>
|
|
</div>
|
|
</div>
|
|
</AccordionSection>
|
|
|
|
<div className="h-6 shrink-0" />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|