diff --git a/backend-node/src/controllers/reportController.ts b/backend-node/src/controllers/reportController.ts index 4c6845fa..124eb265 100644 --- a/backend-node/src/controllers/reportController.ts +++ b/backend-node/src/controllers/reportController.ts @@ -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[] = []; + qResult.rows.forEach((row) => { + const rowData: Record = {}; + 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 = {}; + 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); + } } } } diff --git a/backend-node/src/types/report.ts b/backend-node/src/types/report.ts index f82b2db4..23a7496d 100644 --- a/backend-node/src/types/report.ts +++ b/backend-node/src/types/report.ts @@ -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 값) diff --git a/frontend/components/report/designer/CanvasComponent.tsx b/frontend/components/report/designer/CanvasComponent.tsx index 2eefca42..8816bfe4 100644 --- a/frontend/components/report/designer/CanvasComponent.tsx +++ b/frontend/components/report/designer/CanvasComponent.tsx @@ -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[] = []; + queryResult.rows.forEach((row) => { + const rowData: Record = {}; + 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 = {}; + 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 = {}; + 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"; diff --git a/frontend/components/report/designer/ReportDesignerRightPanel.tsx b/frontend/components/report/designer/ReportDesignerRightPanel.tsx index 635ffd9e..c5b53e25 100644 --- a/frontend/components/report/designer/ReportDesignerRightPanel.tsx +++ b/frontend/components/report/designer/ReportDesignerRightPanel.tsx @@ -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 && ( -
- - + updateComponent(selectedComponent.id, { + qrUseMultiField: e.target.checked, + // 다중 필드 모드 활성화 시 단일 필드 초기화 + ...(e.target.checked && { barcodeFieldName: "" }), + }) + } + className="h-4 w-4 rounded border-gray-300" + /> + +
+ )} + + {/* 단일 필드 모드 (1D 바코드 또는 QR 단일 모드) */} + {(selectedComponent.barcodeType !== "QR" || !selectedComponent.qrUseMultiField) && ( +
+ + +
+ )} + + {/* QR코드 다중 필드 모드 UI */} + {selectedComponent.barcodeType === "QR" && selectedComponent.qrUseMultiField && ( +
+
+ + +
+ + {/* 필드 목록 */} +
+ {(selectedComponent.qrDataFields || []).map((field, index) => ( +
+
+ + { + const newFields = [...(selectedComponent.qrDataFields || [])]; + newFields[index] = { ...newFields[index], label: e.target.value }; + updateComponent(selectedComponent.id, { qrDataFields: newFields }); + }} + placeholder="JSON 키 이름" + className="h-7 text-xs" + /> +
+ +
+ ))} +
+ + {(selectedComponent.qrDataFields || []).length === 0 && ( +

+ 필드를 추가하세요 +

+ )} + +

+ 결과: {selectedComponent.qrIncludeAllRows + ? `[{"${(selectedComponent.qrDataFields || []).map(f => f.label || "key").join('":"값","')}"}, ...]` + : `{"${(selectedComponent.qrDataFields || []).map(f => f.label || "key").join('":"값","')}":"값"}` + } +

+
+ )} + + )} + + {/* QR코드 모든 행 포함 옵션 (다중 필드와 독립) */} + {selectedComponent.barcodeType === "QR" && selectedComponent.queryId && ( +
+ updateComponent(selectedComponent.id, { - barcodeFieldName: value === "none" ? "" : value, + qrIncludeAllRows: e.target.checked, }) } - > - - - - - 선택 안함 - {(() => { - 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) => ( - - {field} - - )); - } - return null; - })()} - - + className="h-4 w-4 rounded border-gray-300" + /> +
)} diff --git a/frontend/types/report.ts b/frontend/types/report.ts index 9a01638a..5a61e5b9 100644 --- a/frontend/types/report.ts +++ b/frontend/types/report.ts @@ -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 값)