345 lines
14 KiB
TypeScript
345 lines
14 KiB
TypeScript
"use client";
|
|
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Calculator } from "lucide-react";
|
|
import { useReportDesigner } from "@/contexts/ReportDesignerContext";
|
|
import type { ComponentConfig } from "@/types/report";
|
|
|
|
interface Props {
|
|
component: ComponentConfig;
|
|
/** 우측 패널: "style" | 모달: "data" | 미전달: 전체 표시 (하위 호환) */
|
|
section?: "style" | "data";
|
|
}
|
|
|
|
export function CalculationProperties({ component, section }: Props) {
|
|
const { updateComponent, queries, getQueryResult } = useReportDesigner();
|
|
|
|
const showStyle = !section || section === "style";
|
|
const showData = !section || section === "data";
|
|
|
|
return (
|
|
<>
|
|
{/* 표시 설정 — 우측 패널(section="style")에서 표시 */}
|
|
{showStyle && (
|
|
<div className="mt-4 space-y-3 rounded-xl border border-orange-200 bg-orange-50/50 p-4">
|
|
<div className="flex items-center gap-2 text-sm font-semibold text-orange-700">
|
|
<Calculator className="h-4 w-4" />
|
|
표시 설정
|
|
</div>
|
|
|
|
{/* 라벨 너비 */}
|
|
<div>
|
|
<Label className="text-xs">라벨 너비 (px)</Label>
|
|
<Input
|
|
type="number"
|
|
value={component.labelWidth || 120}
|
|
onChange={(e) => updateComponent(component.id, { labelWidth: Number(e.target.value) })}
|
|
min={60}
|
|
max={200}
|
|
className="h-9"
|
|
/>
|
|
</div>
|
|
|
|
{/* 숫자 포맷 */}
|
|
<div>
|
|
<Label className="text-xs">숫자 포맷</Label>
|
|
<Select
|
|
value={component.numberFormat || "currency"}
|
|
onValueChange={(value) =>
|
|
updateComponent(component.id, { numberFormat: value as "none" | "comma" | "currency" })
|
|
}
|
|
>
|
|
<SelectTrigger className="h-9">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="none">없음</SelectItem>
|
|
<SelectItem value="comma">천단위 구분</SelectItem>
|
|
<SelectItem value="currency">통화 (원)</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 통화 접미사 */}
|
|
{component.numberFormat === "currency" && (
|
|
<div>
|
|
<Label className="text-xs">통화 단위</Label>
|
|
<Input
|
|
type="text"
|
|
value={component.currencySuffix || "원"}
|
|
onChange={(e) => updateComponent(component.id, { currencySuffix: e.target.value })}
|
|
placeholder="원"
|
|
className="h-9"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* 폰트 크기 설정 */}
|
|
<div className="grid grid-cols-3 gap-2">
|
|
<div>
|
|
<Label className="text-xs">라벨 크기</Label>
|
|
<Input
|
|
type="number"
|
|
value={component.labelFontSize || 13}
|
|
onChange={(e) => updateComponent(component.id, { labelFontSize: Number(e.target.value) })}
|
|
min={10}
|
|
max={20}
|
|
className="h-9"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs">값 크기</Label>
|
|
<Input
|
|
type="number"
|
|
value={component.valueFontSize || 13}
|
|
onChange={(e) => updateComponent(component.id, { valueFontSize: Number(e.target.value) })}
|
|
min={10}
|
|
max={20}
|
|
className="h-9"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs">결과 크기</Label>
|
|
<Input
|
|
type="number"
|
|
value={component.resultFontSize || 16}
|
|
onChange={(e) => updateComponent(component.id, { resultFontSize: Number(e.target.value) })}
|
|
min={12}
|
|
max={24}
|
|
className="h-9"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 색상 설정 */}
|
|
<div className="grid grid-cols-3 gap-2">
|
|
<div>
|
|
<Label className="text-xs">라벨 색상</Label>
|
|
<Input
|
|
type="color"
|
|
value={component.labelColor || "#374151"}
|
|
onChange={(e) => updateComponent(component.id, { labelColor: e.target.value })}
|
|
className="h-9 w-full cursor-pointer p-1"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs">값 색상</Label>
|
|
<Input
|
|
type="color"
|
|
value={component.valueColor || "#000000"}
|
|
onChange={(e) => updateComponent(component.id, { valueColor: e.target.value })}
|
|
className="h-9 w-full cursor-pointer p-1"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs">결과 색상</Label>
|
|
<Input
|
|
type="color"
|
|
value={component.resultColor || "#2563eb"}
|
|
onChange={(e) => updateComponent(component.id, { resultColor: e.target.value })}
|
|
className="h-9 w-full cursor-pointer p-1"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 계산 항목 — 모달(section="data")에서 표시 */}
|
|
{showData && (
|
|
<div className="mt-4 space-y-3 rounded-xl border border-orange-200 bg-orange-50/50 p-4">
|
|
<div className="flex items-center gap-2 text-sm font-semibold text-orange-700">
|
|
<Calculator className="h-4 w-4" />
|
|
계산 항목
|
|
</div>
|
|
|
|
{/* 결과 라벨 */}
|
|
<div>
|
|
<Label className="text-xs">결과 라벨</Label>
|
|
<Input
|
|
type="text"
|
|
value={component.resultLabel || "합계"}
|
|
onChange={(e) => updateComponent(component.id, { resultLabel: e.target.value })}
|
|
placeholder="합계 금액"
|
|
className="h-9"
|
|
/>
|
|
</div>
|
|
|
|
{/* 쿼리 선택 (데이터 바인딩용) */}
|
|
<div>
|
|
<Label className="text-xs">데이터 소스 (쿼리)</Label>
|
|
<Select
|
|
value={component.queryId || "none"}
|
|
onValueChange={(value) =>
|
|
updateComponent(component.id, { queryId: value === "none" ? undefined : value })
|
|
}
|
|
>
|
|
<SelectTrigger className="h-9">
|
|
<SelectValue placeholder="쿼리 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="none">직접 입력</SelectItem>
|
|
{queries.map((q) => (
|
|
<SelectItem key={q.id} value={q.id}>
|
|
{q.name} ({q.type})
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 계산 항목 목록 관리 */}
|
|
<div className="border-t pt-3">
|
|
<div className="mb-2 flex items-center justify-between">
|
|
<Label className="text-xs font-semibold">항목 목록</Label>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
className="h-6 text-xs"
|
|
onClick={() => {
|
|
const currentItems = component.calcItems || [];
|
|
updateComponent(component.id, {
|
|
calcItems: [
|
|
...currentItems,
|
|
{
|
|
label: `항목${currentItems.length + 1}`,
|
|
value: 0,
|
|
operator: "+" as const,
|
|
fieldName: "",
|
|
},
|
|
],
|
|
});
|
|
}}
|
|
>
|
|
+ 항목 추가
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 항목 리스트 — 개별 항목 카드(rounded border bg-white p-2)는 유지 */}
|
|
<div className="max-h-48 space-y-2 overflow-y-auto">
|
|
{(component.calcItems || []).map((item, index: number) => (
|
|
<div key={index} className="rounded border bg-white p-2">
|
|
<div className="mb-1 flex items-center justify-between">
|
|
<span className="text-xs font-medium">항목 {index + 1}</span>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
className="h-5 w-5 p-0 text-red-500 hover:text-red-700"
|
|
onClick={() => {
|
|
const currentItems = [...(component.calcItems || [])];
|
|
currentItems.splice(index, 1);
|
|
updateComponent(component.id, { calcItems: currentItems });
|
|
}}
|
|
>
|
|
x
|
|
</Button>
|
|
</div>
|
|
<div className={`grid gap-1 ${index === 0 ? "grid-cols-1" : "grid-cols-3"}`}>
|
|
<div className={index === 0 ? "" : "col-span-2"}>
|
|
<Label className="text-[10px]">라벨</Label>
|
|
<Input
|
|
type="text"
|
|
value={item.label}
|
|
onChange={(e) => {
|
|
const currentItems = [...(component.calcItems || [])];
|
|
currentItems[index] = { ...currentItems[index], label: e.target.value };
|
|
updateComponent(component.id, { calcItems: currentItems });
|
|
}}
|
|
className="h-6 text-xs"
|
|
placeholder="항목명"
|
|
/>
|
|
</div>
|
|
{index > 0 && (
|
|
<div>
|
|
<Label className="text-[10px]">연산자</Label>
|
|
<Select
|
|
value={item.operator}
|
|
onValueChange={(value) => {
|
|
const currentItems = [...(component.calcItems || [])];
|
|
currentItems[index] = {
|
|
...currentItems[index],
|
|
operator: value as "+" | "-" | "x" | "÷",
|
|
};
|
|
updateComponent(component.id, { calcItems: currentItems });
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-6 text-xs">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="+">+</SelectItem>
|
|
<SelectItem value="-">-</SelectItem>
|
|
<SelectItem value="x">x</SelectItem>
|
|
<SelectItem value="÷">÷</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="mt-1">
|
|
{component.queryId ? (
|
|
<div>
|
|
<Label className="text-[10px]">필드</Label>
|
|
<Select
|
|
value={item.fieldName || "none"}
|
|
onValueChange={(value) => {
|
|
const currentItems = [...(component.calcItems || [])];
|
|
currentItems[index] = {
|
|
...currentItems[index],
|
|
fieldName: value === "none" ? "" : value,
|
|
};
|
|
updateComponent(component.id, { calcItems: currentItems });
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-6 text-xs">
|
|
<SelectValue placeholder="필드 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="none">직접 입력</SelectItem>
|
|
{(() => {
|
|
const query = queries.find((q) => q.id === component.queryId);
|
|
const result = query ? getQueryResult(query.id) : null;
|
|
if (result && result.fields) {
|
|
return result.fields.map((field: string) => (
|
|
<SelectItem key={field} value={field}>
|
|
{field}
|
|
</SelectItem>
|
|
));
|
|
}
|
|
return null;
|
|
})()}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
) : (
|
|
<div>
|
|
<Label className="text-[10px]">값</Label>
|
|
<Input
|
|
type="number"
|
|
value={item.value}
|
|
onChange={(e) => {
|
|
const currentItems = [...(component.calcItems || [])];
|
|
currentItems[index] = {
|
|
...currentItems[index],
|
|
value: Number(e.target.value),
|
|
};
|
|
updateComponent(component.id, { calcItems: currentItems });
|
|
}}
|
|
className="h-6 text-xs"
|
|
placeholder="0"
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
}
|