QR코드 다중 필드 JSON 및 모든 행 포함 기능 추가
This commit is contained in:
parent
acc867e38d
commit
2b912105a8
|
|
@ -1443,13 +1443,66 @@ export class ReportController {
|
|||
|
||||
// 바코드 값 결정 (쿼리 바인딩 또는 고정값)
|
||||
let barcodeValue = component.barcodeValue || "SAMPLE123";
|
||||
if (component.barcodeFieldName && component.queryId && queryResultsMapRef[component.queryId]) {
|
||||
|
||||
// QR코드 다중 필드 모드
|
||||
if (
|
||||
barcodeType === "QR" &&
|
||||
component.qrUseMultiField &&
|
||||
component.qrDataFields &&
|
||||
component.qrDataFields.length > 0 &&
|
||||
component.queryId &&
|
||||
queryResultsMapRef[component.queryId]
|
||||
) {
|
||||
const qResult = queryResultsMapRef[component.queryId];
|
||||
if (qResult.rows && qResult.rows.length > 0) {
|
||||
const row = qResult.rows[0];
|
||||
const val = row[component.barcodeFieldName];
|
||||
if (val !== null && val !== undefined) {
|
||||
barcodeValue = String(val);
|
||||
// 모든 행 포함 모드
|
||||
if (component.qrIncludeAllRows) {
|
||||
const allRowsData: Record<string, string>[] = [];
|
||||
qResult.rows.forEach((row) => {
|
||||
const rowData: Record<string, string> = {};
|
||||
component.qrDataFields!.forEach((field: { fieldName: string; label: string }) => {
|
||||
if (field.fieldName && field.label) {
|
||||
const val = row[field.fieldName];
|
||||
rowData[field.label] = val !== null && val !== undefined ? String(val) : "";
|
||||
}
|
||||
});
|
||||
allRowsData.push(rowData);
|
||||
});
|
||||
barcodeValue = JSON.stringify(allRowsData);
|
||||
} else {
|
||||
// 단일 행 (첫 번째 행만)
|
||||
const row = qResult.rows[0];
|
||||
const jsonData: Record<string, string> = {};
|
||||
component.qrDataFields.forEach((field: { fieldName: string; label: string }) => {
|
||||
if (field.fieldName && field.label) {
|
||||
const val = row[field.fieldName];
|
||||
jsonData[field.label] = val !== null && val !== undefined ? String(val) : "";
|
||||
}
|
||||
});
|
||||
barcodeValue = JSON.stringify(jsonData);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 단일 필드 바인딩
|
||||
else if (component.barcodeFieldName && component.queryId && queryResultsMapRef[component.queryId]) {
|
||||
const qResult = queryResultsMapRef[component.queryId];
|
||||
if (qResult.rows && qResult.rows.length > 0) {
|
||||
// QR코드 + 모든 행 포함
|
||||
if (barcodeType === "QR" && component.qrIncludeAllRows) {
|
||||
const allValues = qResult.rows
|
||||
.map((row) => {
|
||||
const val = row[component.barcodeFieldName!];
|
||||
return val !== null && val !== undefined ? String(val) : "";
|
||||
})
|
||||
.filter((v) => v !== "");
|
||||
barcodeValue = JSON.stringify(allValues);
|
||||
} else {
|
||||
// 단일 행 (첫 번째 행만)
|
||||
const row = qResult.rows[0];
|
||||
const val = row[component.barcodeFieldName];
|
||||
if (val !== null && val !== undefined) {
|
||||
barcodeValue = String(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -260,6 +260,13 @@ export interface ComponentConfig {
|
|||
barcodeBackground?: string;
|
||||
barcodeMargin?: number;
|
||||
qrErrorCorrectionLevel?: "L" | "M" | "Q" | "H";
|
||||
// QR코드 다중 필드 (JSON 형식)
|
||||
qrDataFields?: Array<{
|
||||
fieldName: string;
|
||||
label: string;
|
||||
}>;
|
||||
qrUseMultiField?: boolean;
|
||||
qrIncludeAllRows?: boolean;
|
||||
// 체크박스 컴포넌트 전용
|
||||
checkboxChecked?: boolean; // 체크 상태 (고정값)
|
||||
checkboxFieldName?: string; // 쿼리 필드 바인딩 (truthy/falsy 값)
|
||||
|
|
|
|||
|
|
@ -970,15 +970,81 @@ export function CanvasComponent({ component }: CanvasComponentProps) {
|
|||
|
||||
// 바코드 값 결정 (쿼리 바인딩 또는 고정값)
|
||||
const getBarcodeValue = (): string => {
|
||||
// QR코드 다중 필드 모드
|
||||
if (
|
||||
barcodeType === "QR" &&
|
||||
component.qrUseMultiField &&
|
||||
component.qrDataFields &&
|
||||
component.qrDataFields.length > 0 &&
|
||||
component.queryId
|
||||
) {
|
||||
const queryResult = getQueryResult(component.queryId);
|
||||
if (queryResult && queryResult.rows && queryResult.rows.length > 0) {
|
||||
// 모든 행 포함 모드
|
||||
if (component.qrIncludeAllRows) {
|
||||
const allRowsData: Record<string, string>[] = [];
|
||||
queryResult.rows.forEach((row) => {
|
||||
const rowData: Record<string, string> = {};
|
||||
component.qrDataFields!.forEach((field) => {
|
||||
if (field.fieldName && field.label) {
|
||||
const val = row[field.fieldName];
|
||||
rowData[field.label] = val !== null && val !== undefined ? String(val) : "";
|
||||
}
|
||||
});
|
||||
allRowsData.push(rowData);
|
||||
});
|
||||
return JSON.stringify(allRowsData);
|
||||
}
|
||||
|
||||
// 단일 행 (첫 번째 행만)
|
||||
const row = queryResult.rows[0];
|
||||
const jsonData: Record<string, string> = {};
|
||||
component.qrDataFields.forEach((field) => {
|
||||
if (field.fieldName && field.label) {
|
||||
const val = row[field.fieldName];
|
||||
jsonData[field.label] = val !== null && val !== undefined ? String(val) : "";
|
||||
}
|
||||
});
|
||||
return JSON.stringify(jsonData);
|
||||
}
|
||||
// 쿼리 결과가 없으면 플레이스홀더 표시
|
||||
const placeholderData: Record<string, string> = {};
|
||||
component.qrDataFields.forEach((field) => {
|
||||
if (field.label) {
|
||||
placeholderData[field.label] = `{${field.fieldName || "field"}}`;
|
||||
}
|
||||
});
|
||||
return component.qrIncludeAllRows
|
||||
? JSON.stringify([placeholderData, { "...": "..." }])
|
||||
: JSON.stringify(placeholderData);
|
||||
}
|
||||
|
||||
// 단일 필드 바인딩
|
||||
if (component.barcodeFieldName && component.queryId) {
|
||||
const queryResult = getQueryResult(component.queryId);
|
||||
if (queryResult && queryResult.rows && queryResult.rows.length > 0) {
|
||||
// QR코드 + 모든 행 포함
|
||||
if (barcodeType === "QR" && component.qrIncludeAllRows) {
|
||||
const allValues = queryResult.rows
|
||||
.map((row) => {
|
||||
const val = row[component.barcodeFieldName!];
|
||||
return val !== null && val !== undefined ? String(val) : "";
|
||||
})
|
||||
.filter((v) => v !== "");
|
||||
return JSON.stringify(allValues);
|
||||
}
|
||||
|
||||
// 단일 행 (첫 번째 행만)
|
||||
const row = queryResult.rows[0];
|
||||
const val = row[component.barcodeFieldName];
|
||||
if (val !== null && val !== undefined) {
|
||||
return String(val);
|
||||
}
|
||||
}
|
||||
// 플레이스홀더
|
||||
if (barcodeType === "QR" && component.qrIncludeAllRows) {
|
||||
return JSON.stringify([`{${component.barcodeFieldName}}`, "..."]);
|
||||
}
|
||||
return `{${component.barcodeFieldName}}`;
|
||||
}
|
||||
return component.barcodeValue || "SAMPLE123";
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { Textarea } from "@/components/ui/textarea";
|
|||
import { Label } from "@/components/ui/label";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Trash2, Settings, Database, Link2, Upload, Loader2 } from "lucide-react";
|
||||
import { Trash2, Settings, Database, Link2, Upload, Loader2, X } from "lucide-react";
|
||||
import { useReportDesigner } from "@/contexts/ReportDesignerContext";
|
||||
import { QueryManager } from "./QueryManager";
|
||||
import { SignaturePad } from "./SignaturePad";
|
||||
|
|
@ -1714,35 +1714,183 @@ export function ReportDesignerRightPanel() {
|
|||
|
||||
{/* 쿼리 연결 시 필드 선택 */}
|
||||
{selectedComponent.queryId && (
|
||||
<div>
|
||||
<Label className="text-xs">바인딩 필드</Label>
|
||||
<Select
|
||||
value={selectedComponent.barcodeFieldName || "none"}
|
||||
onValueChange={(value) =>
|
||||
<>
|
||||
{/* QR코드: 다중 필드 모드 토글 */}
|
||||
{selectedComponent.barcodeType === "QR" && (
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="qrUseMultiField"
|
||||
checked={selectedComponent.qrUseMultiField === true}
|
||||
onChange={(e) =>
|
||||
updateComponent(selectedComponent.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 단일 모드) */}
|
||||
{(selectedComponent.barcodeType !== "QR" || !selectedComponent.qrUseMultiField) && (
|
||||
<div>
|
||||
<Label className="text-xs">바인딩 필드</Label>
|
||||
<Select
|
||||
value={selectedComponent.barcodeFieldName || "none"}
|
||||
onValueChange={(value) =>
|
||||
updateComponent(selectedComponent.id, {
|
||||
barcodeFieldName: value === "none" ? "" : value,
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-8">
|
||||
<SelectValue placeholder="필드 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">선택 안함</SelectItem>
|
||||
{(() => {
|
||||
const query = queries.find((q) => q.id === selectedComponent.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 */}
|
||||
{selectedComponent.barcodeType === "QR" && selectedComponent.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 = selectedComponent.qrDataFields || [];
|
||||
updateComponent(selectedComponent.id, {
|
||||
qrDataFields: [...currentFields, { fieldName: "", label: "" }],
|
||||
});
|
||||
}}
|
||||
>
|
||||
+ 필드 추가
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 필드 목록 */}
|
||||
<div className="max-h-[200px] space-y-2 overflow-y-auto">
|
||||
{(selectedComponent.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 = [...(selectedComponent.qrDataFields || [])];
|
||||
newFields[index] = {
|
||||
...newFields[index],
|
||||
fieldName: value === "none" ? "" : value,
|
||||
// 라벨이 비어있으면 필드명으로 자동 설정
|
||||
label: newFields[index].label || (value === "none" ? "" : value),
|
||||
};
|
||||
updateComponent(selectedComponent.id, { qrDataFields: newFields });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs">
|
||||
<SelectValue placeholder="필드 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">선택 안함</SelectItem>
|
||||
{(() => {
|
||||
const query = queries.find((q) => q.id === selectedComponent.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 = [...(selectedComponent.qrDataFields || [])];
|
||||
newFields[index] = { ...newFields[index], label: e.target.value };
|
||||
updateComponent(selectedComponent.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 = (selectedComponent.qrDataFields || []).filter(
|
||||
(_, i) => i !== index
|
||||
);
|
||||
updateComponent(selectedComponent.id, { qrDataFields: newFields });
|
||||
}}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{(selectedComponent.qrDataFields || []).length === 0 && (
|
||||
<p className="text-center text-xs text-gray-400">
|
||||
필드를 추가하세요
|
||||
</p>
|
||||
)}
|
||||
|
||||
<p className="text-[10px] text-gray-500">
|
||||
결과: {selectedComponent.qrIncludeAllRows
|
||||
? `[{"${(selectedComponent.qrDataFields || []).map(f => f.label || "key").join('":"값","')}"}, ...]`
|
||||
: `{"${(selectedComponent.qrDataFields || []).map(f => f.label || "key").join('":"값","')}":"값"}`
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* QR코드 모든 행 포함 옵션 (다중 필드와 독립) */}
|
||||
{selectedComponent.barcodeType === "QR" && selectedComponent.queryId && (
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="qrIncludeAllRows"
|
||||
checked={selectedComponent.qrIncludeAllRows === true}
|
||||
onChange={(e) =>
|
||||
updateComponent(selectedComponent.id, {
|
||||
barcodeFieldName: value === "none" ? "" : value,
|
||||
qrIncludeAllRows: e.target.checked,
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-8">
|
||||
<SelectValue placeholder="필드 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">선택 안함</SelectItem>
|
||||
{(() => {
|
||||
const query = queries.find((q) => q.id === selectedComponent.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>
|
||||
className="h-4 w-4 rounded border-gray-300"
|
||||
/>
|
||||
<Label htmlFor="qrIncludeAllRows" className="text-xs">
|
||||
모든 행 포함 (배열)
|
||||
</Label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -198,6 +198,13 @@ export interface ComponentConfig {
|
|||
barcodeBackground?: string; // 배경 색상
|
||||
barcodeMargin?: number; // 여백
|
||||
qrErrorCorrectionLevel?: "L" | "M" | "Q" | "H"; // QR 오류 보정 수준
|
||||
// QR코드 다중 필드 (JSON 형식)
|
||||
qrDataFields?: Array<{
|
||||
fieldName: string; // 쿼리 필드명
|
||||
label: string; // JSON 키 이름
|
||||
}>;
|
||||
qrUseMultiField?: boolean; // 다중 필드 사용 여부
|
||||
qrIncludeAllRows?: boolean; // 모든 행 포함 (배열 JSON)
|
||||
// 체크박스 컴포넌트 전용
|
||||
checkboxChecked?: boolean; // 체크 상태 (고정값)
|
||||
checkboxFieldName?: string; // 쿼리 필드 바인딩 (truthy/falsy 값)
|
||||
|
|
|
|||
Loading…
Reference in New Issue