ERP-node/frontend/components/report/designer/properties/CardProperties.tsx

257 lines
10 KiB
TypeScript

"use client";
/**
* CardProperties.tsx — 카드 컴포넌트 설정
*
* - section="data": 모달 내 기능 설정 탭에서 CardLayoutTabs 직접 렌더링
* - section="style": 우측 패널에서 프리셋 + 8개 스타일 섹션 제공
* - onConfigChange: Draft 모드 — 모달에서 저장 전 로컬 변경용
*/
import { useMemo, useCallback, useState } from "react";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { ChevronRight } from "lucide-react";
import { useReportDesigner } from "@/contexts/ReportDesignerContext";
import type { ComponentConfig, CardLayoutConfig } from "@/types/report";
import { CardLayoutTabs } from "../modals/CardLayoutTabs";
interface Props {
component: ComponentConfig;
section?: "style" | "data";
onConfigChange?: (updates: Partial<ComponentConfig>) => void;
}
const generateId = () =>
`row_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
const DEFAULT_CONFIG: CardLayoutConfig = {
tableName: "",
primaryKey: "",
rows: [{ id: generateId(), gridColumns: 2, elements: [] }],
padding: "12px",
gap: "8px",
borderStyle: "solid",
borderColor: "#e5e7eb",
backgroundColor: "#ffffff",
headerTitleFontSize: 14,
headerTitleColor: "#1e40af",
labelFontSize: 13,
labelColor: "#374151",
valueFontSize: 13,
valueColor: "#000000",
dividerThickness: 1,
dividerColor: "#e5e7eb",
};
const CARD_STYLE_PRESETS = {
info: {
backgroundColor: "#ffffff",
borderStyle: "solid",
borderColor: "#e5e7eb",
borderWidth: 1,
accentBorderWidth: 0,
borderRadius: "12px",
headerFontWeight: "bold" as const,
headerTitleColor: "#111827",
labelColor: "#6b7280",
valueFontWeight: "normal" as const,
valueColor: "#111827",
dividerColor: "#3b82f6",
dividerThickness: 1,
},
compact: {
backgroundColor: "#eff6ff",
borderStyle: "none",
borderWidth: 0,
accentBorderColor: "#3b82f6",
accentBorderWidth: 4,
borderRadius: "8px",
headerFontWeight: "normal" as const,
headerTitleColor: "#6b7280",
labelColor: "#6b7280",
valueFontWeight: "bold" as const,
valueColor: "#111827",
dividerColor: "#e5e7eb",
dividerThickness: 1,
},
} as const;
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>
);
}
export function CardProperties({ component, section, onConfigChange }: Props) {
const { updateComponent } = useReportDesigner();
const showStyle = !section || section === "style";
const showData = !section || section === "data";
const [openStyleSections, setOpenStyleSections] = useState<Set<string>>(new Set(["preset"]));
const toggleStyleSection = (id: string) => {
setOpenStyleSections((prev) => {
const next = new Set(prev);
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
});
};
const config = useMemo(() => {
return component.cardLayoutConfig || DEFAULT_CONFIG;
}, [component.cardLayoutConfig]);
const handleConfigChange = useCallback(
(newConfig: CardLayoutConfig) => {
const updates = { cardLayoutConfig: newConfig };
if (onConfigChange) onConfigChange(updates);
else updateComponent(component.id, updates);
},
[component.id, onConfigChange, updateComponent],
);
const updateDesignConfig = useCallback(
(updates: Partial<CardLayoutConfig>) => {
handleConfigChange({ ...config, ...updates });
},
[config, handleConfigChange],
);
const applyPreset = useCallback(
(presetKey: keyof typeof CARD_STYLE_PRESETS) => {
updateDesignConfig(CARD_STYLE_PRESETS[presetKey]);
},
[updateDesignConfig],
);
return (
<>
{showData && (
<CardLayoutTabs config={config} onConfigChange={handleConfigChange} component={component} onComponentChange={onConfigChange} />
)}
{showStyle && (
<div className="mt-2 rounded-lg border border-gray-200 overflow-hidden">
<StyleAccordion label="프리셋" isOpen={openStyleSections.has("preset")} onToggle={() => toggleStyleSection("preset")}>
<div className="grid grid-cols-3 gap-2">
<Button variant="outline" size="sm" onClick={() => applyPreset("info")} className="text-xs h-8"> </Button>
<Button variant="outline" size="sm" onClick={() => applyPreset("compact")} className="text-xs h-8"> </Button>
<Button variant="ghost" size="sm" className="text-xs h-8 text-muted-foreground" disabled></Button>
</div>
</StyleAccordion>
<StyleAccordion label="카드 외형" isOpen={openStyleSections.has("appearance")} onToggle={() => toggleStyleSection("appearance")}>
<div className="space-y-3">
<div>
<Label className="mb-1.5 block text-xs text-gray-500"></Label>
<div className="flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-2 py-1.5">
<input type="color" value={config.backgroundColor || "#ffffff"} onChange={(e) => updateDesignConfig({ backgroundColor: e.target.value })} className="h-7 w-7 shrink-0 cursor-pointer rounded border-0 p-0" />
<Input value={config.backgroundColor || "#ffffff"} onChange={(e) => updateDesignConfig({ backgroundColor: e.target.value })} className="h-7 border-0 bg-transparent px-1 font-mono text-xs shadow-none focus-visible:ring-0" />
</div>
</div>
<div className="grid grid-cols-3 gap-2">
<div>
<Label className="mb-1.5 block text-xs text-gray-500"></Label>
<Select value={config.borderRadius || "0"} onValueChange={(v) => updateDesignConfig({ borderRadius: v })}>
<SelectTrigger className="h-9 text-xs"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="0"></SelectItem>
<SelectItem value="8px"></SelectItem>
<SelectItem value="20px"></SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label className="mb-1.5 block text-xs text-gray-500"> </Label>
<Select value={config.padding || "12px"} onValueChange={(v) => updateDesignConfig({ padding: v })}>
<SelectTrigger className="h-9 text-xs"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="2px"></SelectItem>
<SelectItem value="14px"></SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label className="mb-1.5 block text-xs text-gray-500"> </Label>
<Select value={config.gap || "8px"} onValueChange={(v) => updateDesignConfig({ gap: v })}>
<SelectTrigger className="h-9 text-xs"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="1px"></SelectItem>
<SelectItem value="10px"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
</StyleAccordion>
<StyleAccordion label="좌측 액센트 보더" isOpen={openStyleSections.has("accent")} onToggle={() => toggleStyleSection("accent")}>
<div className="space-y-3">
<div className="flex items-center justify-between rounded-lg border border-gray-200 bg-white px-3 py-2">
<Label className="text-xs text-gray-500"></Label>
<Switch checked={(config.accentBorderWidth ?? 0) > 0} onCheckedChange={(checked) => updateDesignConfig({ accentBorderWidth: checked ? 4 : 0, accentBorderColor: config.accentBorderColor || "#3b82f6" })} />
</div>
{(config.accentBorderWidth ?? 0) > 0 && (
<div className="grid grid-cols-2 gap-2">
<div>
<Label className="mb-1.5 block text-xs text-gray-500"></Label>
<Input type="number" min={1} max={10} value={config.accentBorderWidth || 4} onChange={(e) => updateDesignConfig({ accentBorderWidth: parseInt(e.target.value) || 4 })} className="h-9 text-xs" />
</div>
<div>
<Label className="mb-1.5 block text-xs text-gray-500"></Label>
<div className="flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-2 py-1.5">
<input type="color" value={config.accentBorderColor || "#3b82f6"} onChange={(e) => updateDesignConfig({ accentBorderColor: e.target.value })} className="h-7 w-7 shrink-0 cursor-pointer rounded border-0 p-0" />
<Input value={config.accentBorderColor || "#3b82f6"} onChange={(e) => updateDesignConfig({ accentBorderColor: e.target.value })} className="h-7 border-0 bg-transparent px-1 font-mono text-xs shadow-none focus-visible:ring-0" />
</div>
</div>
</div>
)}
</div>
</StyleAccordion>
</div>
)}
</>
);
}