QR코드 다중 필드 JSON 및 모든 행 포함 기능 추가

This commit is contained in:
dohyeons 2025-12-22 13:36:42 +09:00
parent acc867e38d
commit 2b912105a8
5 changed files with 313 additions and 32 deletions

View File

@ -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);
}
}
}
}

View File

@ -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 값)

View File

@ -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";

View File

@ -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>
)}

View File

@ -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 값)