359 lines
16 KiB
TypeScript
359 lines
16 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 { QrCode, X } 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 BarcodeProperties({ 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-cyan-200 bg-cyan-50/50 p-4">
|
|
<div className="flex items-center gap-2 text-sm font-semibold text-cyan-700">
|
|
<QrCode className="h-4 w-4" />
|
|
바코드 스타일
|
|
</div>
|
|
|
|
{/* 1D 바코드 전용: 텍스트 표시 */}
|
|
{component.barcodeType !== "QR" && (
|
|
<div className="flex items-center gap-2">
|
|
<input
|
|
type="checkbox"
|
|
id="showBarcodeText"
|
|
checked={component.showBarcodeText !== false}
|
|
onChange={(e) => updateComponent(component.id, { showBarcodeText: e.target.checked })}
|
|
className="h-4 w-4 rounded border-gray-300"
|
|
/>
|
|
<Label htmlFor="showBarcodeText" className="text-xs">
|
|
바코드 아래 텍스트 표시
|
|
</Label>
|
|
</div>
|
|
)}
|
|
|
|
{/* 색상 설정 */}
|
|
<div className="grid grid-cols-2 gap-2">
|
|
<div>
|
|
<Label className="text-xs">바코드 색상</Label>
|
|
<Input
|
|
type="color"
|
|
value={component.barcodeColor || "#000000"}
|
|
onChange={(e) => updateComponent(component.id, { barcodeColor: e.target.value })}
|
|
className="h-9 w-full"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs">배경 색상</Label>
|
|
<Input
|
|
type="color"
|
|
value={component.barcodeBackground || "#ffffff"}
|
|
onChange={(e) => updateComponent(component.id, { barcodeBackground: e.target.value })}
|
|
className="h-9 w-full"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 여백 */}
|
|
<div>
|
|
<Label className="text-xs">여백 (px)</Label>
|
|
<Input
|
|
type="number"
|
|
value={component.barcodeMargin ?? 10}
|
|
onChange={(e) => updateComponent(component.id, { barcodeMargin: Number(e.target.value) })}
|
|
min={0}
|
|
max={50}
|
|
className="h-9"
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 바코드 데이터 — 모달(section="data")에서 표시 */}
|
|
{showData && (
|
|
<div className="mt-4 space-y-3 rounded-xl border border-cyan-200 bg-cyan-50/50 p-4">
|
|
<div className="flex items-center gap-2 text-sm font-semibold text-cyan-700">
|
|
<QrCode className="h-4 w-4" />
|
|
바코드 데이터
|
|
</div>
|
|
|
|
{/* 바코드 타입 */}
|
|
<div>
|
|
<Label className="text-xs">바코드 타입</Label>
|
|
<Select
|
|
value={component.barcodeType || "CODE128"}
|
|
onValueChange={(value) => {
|
|
const newType = value as "CODE128" | "CODE39" | "EAN13" | "EAN8" | "UPC" | "QR";
|
|
if (newType === "QR") {
|
|
const size = Math.max(component.width, component.height);
|
|
updateComponent(component.id, { barcodeType: newType, width: size, height: size });
|
|
} else {
|
|
updateComponent(component.id, { barcodeType: newType });
|
|
}
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-9">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="CODE128">CODE128 (범용)</SelectItem>
|
|
<SelectItem value="CODE39">CODE39 (산업용)</SelectItem>
|
|
<SelectItem value="EAN13">EAN-13 (상품)</SelectItem>
|
|
<SelectItem value="EAN8">EAN-8 (소형상품)</SelectItem>
|
|
<SelectItem value="UPC">UPC (북미상품)</SelectItem>
|
|
<SelectItem value="QR">QR코드</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* QR 오류 보정 수준 */}
|
|
{component.barcodeType === "QR" && (
|
|
<div>
|
|
<Label className="text-xs">오류 보정 수준</Label>
|
|
<Select
|
|
value={component.qrErrorCorrectionLevel || "M"}
|
|
onValueChange={(value) =>
|
|
updateComponent(component.id, { qrErrorCorrectionLevel: value as "L" | "M" | "Q" | "H" })
|
|
}
|
|
>
|
|
<SelectTrigger className="h-9">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="L">L (7% 복구)</SelectItem>
|
|
<SelectItem value="M">M (15% 복구)</SelectItem>
|
|
<SelectItem value="Q">Q (25% 복구)</SelectItem>
|
|
<SelectItem value="H">H (30% 복구)</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<p className="mt-1 text-[10px] text-gray-500">높을수록 손상에 강하지만 크기 증가</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* 바코드 값 입력 (쿼리 연결 없을 때) */}
|
|
{!component.queryId && (
|
|
<div>
|
|
<Label className="text-xs">바코드 값</Label>
|
|
<Input
|
|
type="text"
|
|
value={component.barcodeValue || ""}
|
|
onChange={(e) => updateComponent(component.id, { barcodeValue: e.target.value })}
|
|
placeholder={
|
|
component.barcodeType === "EAN13"
|
|
? "13자리 숫자"
|
|
: component.barcodeType === "EAN8"
|
|
? "8자리 숫자"
|
|
: component.barcodeType === "UPC"
|
|
? "12자리 숫자"
|
|
: "바코드에 표시할 값"
|
|
}
|
|
className="h-9"
|
|
/>
|
|
{(component.barcodeType === "EAN13" ||
|
|
component.barcodeType === "EAN8" ||
|
|
component.barcodeType === "UPC") && (
|
|
<p className="mt-1 text-[10px] text-gray-500">
|
|
{component.barcodeType === "EAN13" && "EAN-13: 12~13자리 숫자 필요"}
|
|
{component.barcodeType === "EAN8" && "EAN-8: 7~8자리 숫자 필요"}
|
|
{component.barcodeType === "UPC" && "UPC: 11~12자리 숫자 필요"}
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* 쿼리 연결 안내 — 인라인 안내 텍스트 (수정하지 않음) */}
|
|
{!component.queryId && (
|
|
<div className="rounded border border-cyan-200 bg-cyan-100 p-2 text-xs text-cyan-800">
|
|
쿼리를 연결하면 데이터베이스 값으로 바코드를 생성할 수 있습니다.
|
|
</div>
|
|
)}
|
|
|
|
{/* 쿼리 연결 시 필드 선택 */}
|
|
{component.queryId && (
|
|
<>
|
|
{/* QR코드: 다중 필드 모드 토글 */}
|
|
{component.barcodeType === "QR" && (
|
|
<div className="flex items-center gap-2">
|
|
<input
|
|
type="checkbox"
|
|
id="qrUseMultiField"
|
|
checked={component.qrUseMultiField === true}
|
|
onChange={(e) =>
|
|
updateComponent(component.id, {
|
|
qrUseMultiField: e.target.checked,
|
|
...(e.target.checked && { barcodeFieldName: "" }),
|
|
})
|
|
}
|
|
className="h-4 w-4 rounded border-gray-300"
|
|
/>
|
|
<Label htmlFor="qrUseMultiField" className="text-xs">
|
|
다중 필드 (JSON 형식)
|
|
</Label>
|
|
</div>
|
|
)}
|
|
|
|
{/* 단일 필드 모드 (1D 바코드 또는 QR 단일 모드) */}
|
|
{(component.barcodeType !== "QR" || !component.qrUseMultiField) && (
|
|
<div>
|
|
<Label className="text-xs">바인딩 필드</Label>
|
|
<Select
|
|
value={component.barcodeFieldName || "none"}
|
|
onValueChange={(value) =>
|
|
updateComponent(component.id, { barcodeFieldName: value === "none" ? "" : value })
|
|
}
|
|
>
|
|
<SelectTrigger className="h-9">
|
|
<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>
|
|
)}
|
|
|
|
{/* QR코드 다중 필드 모드 UI */}
|
|
{component.barcodeType === "QR" && component.qrUseMultiField && (
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<Label className="text-xs">JSON 필드 매핑</Label>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="h-6 px-2 text-xs"
|
|
onClick={() => {
|
|
const currentFields = component.qrDataFields || [];
|
|
updateComponent(component.id, {
|
|
qrDataFields: [...currentFields, { fieldName: "", label: "" }],
|
|
});
|
|
}}
|
|
>
|
|
+ 필드 추가
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 필드 목록 */}
|
|
<div className="max-h-[200px] space-y-2 overflow-y-auto">
|
|
{(component.qrDataFields || []).map((field, index) => (
|
|
<div key={index} className="flex items-center gap-1 rounded border p-2">
|
|
<div className="flex-1 space-y-1">
|
|
<Select
|
|
value={field.fieldName || "none"}
|
|
onValueChange={(value) => {
|
|
const newFields = [...(component.qrDataFields || [])];
|
|
newFields[index] = {
|
|
...newFields[index],
|
|
fieldName: value === "none" ? "" : value,
|
|
label: newFields[index].label || (value === "none" ? "" : value),
|
|
};
|
|
updateComponent(component.id, { qrDataFields: newFields });
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-7 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((f: string) => (
|
|
<SelectItem key={f} value={f}>
|
|
{f}
|
|
</SelectItem>
|
|
));
|
|
}
|
|
return null;
|
|
})()}
|
|
</SelectContent>
|
|
</Select>
|
|
<Input
|
|
type="text"
|
|
value={field.label || ""}
|
|
onChange={(e) => {
|
|
const newFields = [...(component.qrDataFields || [])];
|
|
newFields[index] = { ...newFields[index], label: e.target.value };
|
|
updateComponent(component.id, { qrDataFields: newFields });
|
|
}}
|
|
placeholder="JSON 키 이름"
|
|
className="h-7 text-xs"
|
|
/>
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-7 w-7 p-0 text-red-500 hover:text-red-700"
|
|
onClick={() => {
|
|
const newFields = (component.qrDataFields || []).filter((_, i) => i !== index);
|
|
updateComponent(component.id, { qrDataFields: newFields });
|
|
}}
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{(component.qrDataFields || []).length === 0 && (
|
|
<p className="text-center text-xs text-gray-400">필드를 추가하세요</p>
|
|
)}
|
|
|
|
<p className="text-[10px] text-gray-500">
|
|
결과:{" "}
|
|
{component.qrIncludeAllRows
|
|
? `[{"${(component.qrDataFields || []).map((f) => f.label || "key").join('":"값","')}"}, ...]`
|
|
: `{"${(component.qrDataFields || []).map((f) => f.label || "key").join('":"값","')}":"값"}`}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* QR코드 모든 행 포함 옵션 */}
|
|
{component.barcodeType === "QR" && component.queryId && (
|
|
<div className="flex items-center gap-2">
|
|
<input
|
|
type="checkbox"
|
|
id="qrIncludeAllRows"
|
|
checked={component.qrIncludeAllRows === true}
|
|
onChange={(e) => updateComponent(component.id, { qrIncludeAllRows: e.target.checked })}
|
|
className="h-4 w-4 rounded border-gray-300"
|
|
/>
|
|
<Label htmlFor="qrIncludeAllRows" className="text-xs">
|
|
모든 행 포함 (배열)
|
|
</Label>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
}
|