feat: 리포트 타입 에러 수정
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
"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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user