194 lines
6.6 KiB
TypeScript
194 lines
6.6 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* DividerProperties.tsx — 구분선 컴포넌트 설정
|
|
*
|
|
* - section="data": 구분선은 데이터 바인딩 없으므로 null 반환
|
|
* - section="style": StyleAccordion 패턴 (방향 & 선 스타일 + 색상)
|
|
*/
|
|
|
|
import { useState, useCallback } 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 { ChevronRight } from "lucide-react";
|
|
import { useReportDesigner } from "@/contexts/ReportDesignerContext";
|
|
import type { ComponentConfig } from "@/types/report";
|
|
|
|
interface Props {
|
|
component: ComponentConfig;
|
|
/** 우측 패널: "style" | 모달: "data" | 미전달: 전체 표시 (하위 호환) */
|
|
section?: "style" | "data";
|
|
}
|
|
|
|
function StyleAccordion({
|
|
label,
|
|
isOpen,
|
|
onToggle,
|
|
children,
|
|
}: {
|
|
label: string;
|
|
isOpen: boolean;
|
|
onToggle: () => void;
|
|
children: React.ReactNode;
|
|
}) {
|
|
return (
|
|
<div className="border-b border-gray-100">
|
|
<button
|
|
onClick={onToggle}
|
|
className={`flex h-10 w-full items-center justify-between px-3 transition-colors ${
|
|
isOpen
|
|
? "bg-linear-to-r from-blue-600 to-indigo-600 text-white shadow-sm"
|
|
: "bg-white text-gray-900 hover:bg-gray-50"
|
|
}`}
|
|
>
|
|
<span className="text-xs font-bold">{label}</span>
|
|
<ChevronRight
|
|
className={`h-3.5 w-3.5 transition-transform ${isOpen ? "rotate-90" : "text-gray-400"}`}
|
|
/>
|
|
</button>
|
|
{isOpen && (
|
|
<div className="border-t border-blue-100 bg-linear-to-b from-blue-50/30 to-white p-3">
|
|
{children}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ColorInput({ value, onChange }: { value: string; onChange: (v: string) => void }) {
|
|
return (
|
|
<div className="flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-2 py-1.5">
|
|
<input
|
|
type="color"
|
|
value={value}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
className="h-7 w-7 shrink-0 cursor-pointer rounded border-0 p-0"
|
|
/>
|
|
<Input
|
|
value={value}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
className="h-7 border-0 bg-transparent px-1 font-mono text-xs shadow-none focus-visible:ring-0"
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function DividerProperties({ component, section }: Props) {
|
|
const { updateComponent } = useReportDesigner();
|
|
|
|
const showStyle = !section || section === "style";
|
|
|
|
if (section === "data") return null;
|
|
|
|
const [openSections, setOpenSections] = useState<Set<string>>(new Set(["line"]));
|
|
const toggleSection = (id: string) => {
|
|
setOpenSections((prev) => {
|
|
const next = new Set(prev);
|
|
if (next.has(id)) next.delete(id);
|
|
else next.add(id);
|
|
return next;
|
|
});
|
|
};
|
|
|
|
const update = useCallback(
|
|
(updates: Partial<ComponentConfig>) => updateComponent(component.id, updates),
|
|
[component.id, updateComponent],
|
|
);
|
|
|
|
return (
|
|
<>
|
|
{showStyle && (
|
|
<div className="mt-2 overflow-hidden rounded-lg border border-gray-200">
|
|
{/* 방향 & 선 스타일 */}
|
|
<StyleAccordion label="방향 & 선 스타일" isOpen={openSections.has("line")} onToggle={() => toggleSection("line")}>
|
|
<div className="space-y-3">
|
|
<div className="grid grid-cols-2 gap-2">
|
|
<div>
|
|
<Label className="mb-1.5 block text-xs text-gray-500">방향</Label>
|
|
<Select
|
|
value={component.orientation || "horizontal"}
|
|
onValueChange={(value) => {
|
|
const isToVertical = value === "vertical";
|
|
const currentWidth = component.width;
|
|
const currentHeight = component.height;
|
|
update({
|
|
orientation: value as "horizontal" | "vertical",
|
|
width: isToVertical ? 10 : currentWidth > 50 ? currentWidth : 300,
|
|
height: isToVertical ? (currentWidth > 50 ? currentWidth : 300) : 10,
|
|
});
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-9 text-xs">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="horizontal">가로</SelectItem>
|
|
<SelectItem value="vertical">세로</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div>
|
|
<Label className="mb-1.5 block text-xs text-gray-500">선 스타일</Label>
|
|
<Select
|
|
value={component.lineStyle || "solid"}
|
|
onValueChange={(value) =>
|
|
update({ lineStyle: value as "solid" | "dashed" | "dotted" | "double" })
|
|
}
|
|
>
|
|
<SelectTrigger className="h-9 text-xs">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="solid">실선</SelectItem>
|
|
<SelectItem value="dashed">파선</SelectItem>
|
|
<SelectItem value="dotted">점선</SelectItem>
|
|
<SelectItem value="double">이중선</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<Label className="mb-1.5 block text-xs text-gray-500">선 두께</Label>
|
|
<Input
|
|
type="number"
|
|
min={0.5}
|
|
max={20}
|
|
step={0.5}
|
|
value={component.lineWidth ?? 1}
|
|
onChange={(e) => {
|
|
const val = parseFloat(e.target.value);
|
|
if (!isNaN(val) && val >= 0.5) {
|
|
update({ lineWidth: val });
|
|
}
|
|
}}
|
|
className="h-9 text-xs"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</StyleAccordion>
|
|
|
|
{/* 색상 */}
|
|
<StyleAccordion label="색상" isOpen={openSections.has("color")} onToggle={() => toggleSection("color")}>
|
|
<div className="space-y-3">
|
|
<div>
|
|
<Label className="mb-1.5 block text-xs text-gray-500">선 색상</Label>
|
|
<ColorInput
|
|
value={component.lineColor || "#000000"}
|
|
onChange={(v) => update({ lineColor: v })}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</StyleAccordion>
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
}
|