116 lines
4.2 KiB
TypeScript
116 lines
4.2 KiB
TypeScript
"use client";
|
|
|
|
import React, { useCallback } from "react";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Plus, Trash2 } from "lucide-react";
|
|
import { ReportParamMapping, ReportViewerConfig } from "./types";
|
|
|
|
interface ReportViewerConfigPanelProps {
|
|
config: ReportViewerConfig;
|
|
onChange: (config: Partial<ReportViewerConfig>) => void;
|
|
}
|
|
|
|
export const ReportViewerConfigPanel: React.FC<ReportViewerConfigPanelProps> = ({ config, onChange }) => {
|
|
const mappings = config.paramMappings ?? [];
|
|
|
|
const handleAddMapping = useCallback(() => {
|
|
onChange({ paramMappings: [...mappings, { param: "", formField: "" }] });
|
|
}, [mappings, onChange]);
|
|
|
|
const handleRemoveMapping = useCallback(
|
|
(index: number) => {
|
|
onChange({ paramMappings: mappings.filter((_, i) => i !== index) });
|
|
},
|
|
[mappings, onChange],
|
|
);
|
|
|
|
const handleMappingChange = useCallback(
|
|
(index: number, field: keyof ReportParamMapping, value: string) => {
|
|
const updated = mappings.map((m, i) => (i === index ? { ...m, [field]: value } : m));
|
|
onChange({ paramMappings: updated });
|
|
},
|
|
[mappings, onChange],
|
|
);
|
|
|
|
return (
|
|
<div className="flex flex-col gap-4 p-4">
|
|
{/* 컴포넌트 제목 */}
|
|
<div className="flex flex-col gap-1.5">
|
|
<Label className="text-xs font-medium text-gray-600">컴포넌트 제목</Label>
|
|
<Input
|
|
value={config.title ?? "리포트"}
|
|
onChange={(e) => onChange({ title: e.target.value })}
|
|
placeholder="리포트"
|
|
className="h-8 text-sm"
|
|
/>
|
|
<p className="text-xs text-gray-400">디자인 모드에서 표시되는 제목입니다.</p>
|
|
</div>
|
|
|
|
{/* 파라미터 매핑 */}
|
|
<div className="flex flex-col gap-2">
|
|
<div className="flex items-center justify-between">
|
|
<Label className="text-xs font-medium text-gray-600">파라미터 매핑</Label>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={handleAddMapping}
|
|
className="h-6 gap-1 px-2 text-xs text-blue-600 hover:text-blue-700"
|
|
>
|
|
<Plus className="h-3 w-3" />
|
|
추가
|
|
</Button>
|
|
</div>
|
|
|
|
{mappings.length === 0 ? (
|
|
<p className="text-xs text-gray-400">매핑 없음 — 폼 데이터가 순서대로 자동 주입됩니다.</p>
|
|
) : (
|
|
<div className="flex flex-col gap-1.5">
|
|
<div className="grid grid-cols-[1fr_1fr_auto] gap-1 text-xs text-gray-400">
|
|
<span>쿼리 파라미터</span>
|
|
<span>폼 데이터 필드</span>
|
|
<span />
|
|
</div>
|
|
{mappings.map((mapping, index) => (
|
|
<div key={index} className="grid grid-cols-[1fr_1fr_auto] items-center gap-1">
|
|
<Input
|
|
value={mapping.param}
|
|
onChange={(e) => handleMappingChange(index, "param", e.target.value)}
|
|
placeholder="예: $1"
|
|
className="h-7 text-xs"
|
|
/>
|
|
<Input
|
|
value={mapping.formField}
|
|
onChange={(e) => handleMappingChange(index, "formField", e.target.value)}
|
|
placeholder="예: orderId"
|
|
className="h-7 text-xs"
|
|
/>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => handleRemoveMapping(index)}
|
|
className="h-7 w-7 p-0 text-gray-400 hover:text-red-500"
|
|
>
|
|
<Trash2 className="h-3.5 w-3.5" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
<p className="text-xs text-gray-400">
|
|
쿼리 파라미터($1, $2 또는 이름)와 현재 화면 폼 데이터 필드를 연결합니다.
|
|
</p>
|
|
</div>
|
|
|
|
{/* 안내 */}
|
|
<div className="rounded border border-blue-100 bg-blue-50 p-3 text-xs text-blue-600">
|
|
메뉴에 연결된 리포트는 리포트 관리 페이지에서 설정합니다.
|
|
<br />
|
|
매핑이 없으면 폼 데이터가 자동으로 순서 기반 주입됩니다.
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|