"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 = { text: "텍스트", label: "레이블", table: "테이블", image: "이미지", divider: "구분선", signature: "서명", stamp: "도장", pageNumber: "페이지 번호", card: "카드", calculation: "계산", barcode: "바코드", checkbox: "체크박스", }; const TYPE_ICONS: Record = { text: , label: , table: , image: , divider: , signature: , stamp: , pageNumber: , card: , calculation: , barcode: , checkbox: , }; 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 (
{isExpanded && (
{children}
)}
); } function TypeSpecificDesign({ component, update, }: { component: ComponentConfig; update: (changes: Partial) => void; }) { const type = component.type; if (type === "text" || type === "label") { return ( ); } if (type === "table") { return ( ); } if (type === "image") { return ( ); } if (type === "divider") { return ( ); } if (type === "signature" || type === "stamp") { return (
update({ showLabel: e.target.checked })} className="h-4 w-4 rounded" />
{component.showLabel !== false && (
update({ labelText: e.target.value })} className="h-9 text-sm" />
)}
); } if (type === "pageNumber") { return (
); } if (type === "calculation") { return (
{component.numberFormat === "currency" && (
update({ currencySuffix: e.target.value })} className="h-9 text-sm" />
)}
update({ resultColor: e.target.value })} className="w-9 h-9 rounded cursor-pointer border border-gray-200" /> update({ resultColor: e.target.value })} className="h-9 text-sm flex-1" />
update({ resultFontSize: parseInt(e.target.value) || 16 })} className="h-9 text-sm" />
); } if (type === "barcode") { return (
update({ barcodeColor: e.target.value })} className="w-9 h-9 rounded cursor-pointer border border-gray-200" />
update({ barcodeBackground: e.target.value })} className="w-9 h-9 rounded cursor-pointer border border-gray-200" />
update({ showBarcodeText: e.target.checked })} className="h-4 w-4 rounded" />
update({ barcodeMargin: parseInt(e.target.value) || 0 })} className="h-9 text-sm" />
); } if (type === "checkbox") { return (
update({ checkboxColor: e.target.value })} className="w-9 h-9 rounded cursor-pointer border border-gray-200" /> update({ checkboxColor: e.target.value })} className="h-9 text-sm flex-1" />
update({ checkboxBorderColor: e.target.value })} className="w-9 h-9 rounded cursor-pointer border border-gray-200" />
update({ checkboxSize: parseInt(e.target.value) || 18 })} className="h-9 text-sm" />
); } if (type === "statusBadge") { return (

상태 매핑은 모달(더블클릭)에서 설정합니다.

); } return null; } export function ComponentStylePanel() { const { selectedComponentId, selectedComponentIds, components, updateComponent, bringToFront, sendToBack, bringForward, sendBackward, toggleLock, groupComponents, ungroupComponents, } = useReportDesigner(); const [expandedSections, setExpandedSections] = useState>(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) => 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] || ; return (
{/* 카드 전용: CardProperties 하위 아코디언 포함 */} {component.type === "card" && ( } isExpanded={expandedSections.has("cardDesign")} onToggle={() => toggleSection("cardDesign")} > )} {/* 카드 외 모든 타입: 타입별 전용 디자인 */} {hasTypeDesign && ( toggleSection("typeDesign")} > )} {/* 배치 (X/Y/W/H) */} } isExpanded={expandedSections.has("layout")} onToggle={() => toggleSection("layout")} >
{([ { 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 }) => (
update({ [key]: parseInt(e.target.value) || min || 0 }) } className="h-9 text-sm" />
))}
{/* 잠금 */} } isExpanded={expandedSections.has("lock")} onToggle={() => toggleSection("lock")} >

잠금 시 드래그/리사이즈 불가

{/* 레이어 */} } isExpanded={expandedSections.has("layer")} onToggle={() => toggleSection("layer")} >
{/* 그룹 */} } isExpanded={expandedSections.has("group")} onToggle={() => toggleSection("group")} > {component.groupId ? (

그룹 내 컴포넌트를 드래그하면 함께 이동합니다.

같은 그룹: {components.filter((c) => c.groupId === component.groupId).length}개

) : (

{selectedComponentIds.length < 2 ? "드래그 또는 Ctrl+클릭으로 2개 이상 선택 후 그룹화" : `${selectedComponentIds.length}개 선택됨 - 그룹화 가능`}

)}
{/* 테두리 */} } isExpanded={expandedSections.has("border")} onToggle={() => toggleSection("border")} >
); }