diff --git a/backend-node/src/controllers/reportController.ts b/backend-node/src/controllers/reportController.ts index 438e02e1..4c6845fa 100644 --- a/backend-node/src/controllers/reportController.ts +++ b/backend-node/src/controllers/reportController.ts @@ -1364,6 +1364,45 @@ export class ReportController { } } + // Checkbox 컴포넌트 + else if (component.type === "checkbox") { + // 체크 상태 결정 (쿼리 바인딩 또는 고정값) + let isChecked = component.checkboxChecked === true; + if (component.checkboxFieldName && 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.checkboxFieldName]; + // truthy/falsy 값 판정 + if (val === true || val === "true" || val === "Y" || val === 1 || val === "1") { + isChecked = true; + } else { + isChecked = false; + } + } + } + + const checkboxSymbol = isChecked ? "☑" : "☐"; + const checkboxLabel = component.checkboxLabel || ""; + const labelPosition = component.checkboxLabelPosition || "right"; + const displayText = labelPosition === "left" + ? `${checkboxLabel} ${checkboxSymbol}` + : `${checkboxSymbol} ${checkboxLabel}`; + + result.push( + new ParagraphRef({ + children: [ + new TextRunRef({ + text: displayText.trim(), + size: pxToHalfPtFn(component.fontSize || 14), + font: "맑은 고딕", + color: (component.fontColor || "#374151").replace("#", ""), + }), + ], + }) + ); + } + // Divider - 테이블 셀로 감싸서 정확한 너비 적용 else if ( component.type === "divider" && @@ -2809,6 +2848,58 @@ export class ReportController { lastBottomY = adjustedY + component.height; } + // Checkbox 컴포넌트 + else if (component.type === "checkbox") { + // 체크 상태 결정 (쿼리 바인딩 또는 고정값) + let isChecked = component.checkboxChecked === true; + if (component.checkboxFieldName && component.queryId && queryResultsMap[component.queryId]) { + const qResult = queryResultsMap[component.queryId]; + if (qResult.rows && qResult.rows.length > 0) { + const row = qResult.rows[0]; + const val = row[component.checkboxFieldName]; + // truthy/falsy 값 판정 + if (val === true || val === "true" || val === "Y" || val === 1 || val === "1") { + isChecked = true; + } else { + isChecked = false; + } + } + } + + const checkboxSymbol = isChecked ? "☑" : "☐"; + const checkboxLabel = component.checkboxLabel || ""; + const labelPosition = component.checkboxLabelPosition || "right"; + const displayText = labelPosition === "left" + ? `${checkboxLabel} ${checkboxSymbol}` + : `${checkboxSymbol} ${checkboxLabel}`; + + // spacing을 위한 빈 paragraph + if (spacingBefore > 0) { + children.push( + new Paragraph({ + spacing: { before: spacingBefore, after: 0 }, + children: [], + }) + ); + } + + children.push( + new Paragraph({ + indent: { left: indentLeft }, + children: [ + new TextRun({ + text: displayText.trim(), + size: pxToHalfPt(component.fontSize || 14), + font: "맑은 고딕", + color: (component.fontColor || "#374151").replace("#", ""), + }), + ], + }) + ); + + lastBottomY = adjustedY + component.height; + } + // Table 컴포넌트 else if (component.type === "table" && component.queryId) { const queryResult = queryResultsMap[component.queryId]; diff --git a/backend-node/src/types/report.ts b/backend-node/src/types/report.ts index e622a65c..f82b2db4 100644 --- a/backend-node/src/types/report.ts +++ b/backend-node/src/types/report.ts @@ -260,4 +260,12 @@ export interface ComponentConfig { barcodeBackground?: string; barcodeMargin?: number; qrErrorCorrectionLevel?: "L" | "M" | "Q" | "H"; + // 체크박스 컴포넌트 전용 + checkboxChecked?: boolean; // 체크 상태 (고정값) + checkboxFieldName?: string; // 쿼리 필드 바인딩 (truthy/falsy 값) + checkboxLabel?: string; // 체크박스 옆 레이블 텍스트 + checkboxSize?: number; // 체크박스 크기 (px) + checkboxColor?: string; // 체크 색상 + checkboxBorderColor?: string; // 테두리 색상 + checkboxLabelPosition?: "left" | "right"; // 레이블 위치 } diff --git a/frontend/components/report/designer/CanvasComponent.tsx b/frontend/components/report/designer/CanvasComponent.tsx index e3f06e9f..12011813 100644 --- a/frontend/components/report/designer/CanvasComponent.tsx +++ b/frontend/components/report/designer/CanvasComponent.tsx @@ -1002,6 +1002,89 @@ export function CanvasComponent({ component }: CanvasComponentProps) { ); + case "checkbox": + // 체크박스 컴포넌트 렌더링 + const checkboxSize = component.checkboxSize || 18; + const checkboxColor = component.checkboxColor || "#2563eb"; + const checkboxBorderColor = component.checkboxBorderColor || "#6b7280"; + const checkboxLabelPosition = component.checkboxLabelPosition || "right"; + const checkboxLabel = component.checkboxLabel || ""; + + // 체크 상태 결정 (쿼리 바인딩 또는 고정값) + const getCheckboxValue = (): boolean => { + if (component.checkboxFieldName && component.queryId) { + const queryResult = getQueryResult(component.queryId); + if (queryResult && queryResult.rows && queryResult.rows.length > 0) { + const row = queryResult.rows[0]; + const val = row[component.checkboxFieldName]; + // truthy/falsy 값 판정 + if (val === true || val === "true" || val === "Y" || val === 1 || val === "1") { + return true; + } + return false; + } + return false; + } + return component.checkboxChecked === true; + }; + + const isChecked = getCheckboxValue(); + + return ( +
+
+ 체크박스 + {component.checkboxFieldName && component.queryId && ( + ● 연결됨 + )} +
+
+ {/* 체크박스 */} +
+ {isChecked && ( + + + + )} +
+ {/* 레이블 */} + {checkboxLabel && ( + + {checkboxLabel} + + )} +
+
+ ); + default: return
알 수 없는 컴포넌트
; } diff --git a/frontend/components/report/designer/ComponentPalette.tsx b/frontend/components/report/designer/ComponentPalette.tsx index b7bdcbb0..68f445c4 100644 --- a/frontend/components/report/designer/ComponentPalette.tsx +++ b/frontend/components/report/designer/ComponentPalette.tsx @@ -1,7 +1,7 @@ "use client"; import { useDrag } from "react-dnd"; -import { Type, Table, Image, Minus, PenLine, Stamp as StampIcon, Hash, CreditCard, Calculator, Barcode } from "lucide-react"; +import { Type, Table, Image, Minus, PenLine, Stamp as StampIcon, Hash, CreditCard, Calculator, Barcode, CheckSquare } from "lucide-react"; interface ComponentItem { type: string; @@ -20,6 +20,7 @@ const COMPONENTS: ComponentItem[] = [ { type: "card", label: "정보카드", icon: }, { type: "calculation", label: "계산", icon: }, { type: "barcode", label: "바코드/QR", icon: }, + { type: "checkbox", label: "체크박스", icon: }, ]; function DraggableComponentItem({ type, label, icon }: ComponentItem) { diff --git a/frontend/components/report/designer/ReportDesignerCanvas.tsx b/frontend/components/report/designer/ReportDesignerCanvas.tsx index c4ea2a27..e63d1a9d 100644 --- a/frontend/components/report/designer/ReportDesignerCanvas.tsx +++ b/frontend/components/report/designer/ReportDesignerCanvas.tsx @@ -71,6 +71,9 @@ export function ReportDesignerCanvas() { } else if (item.componentType === "barcode") { width = 200; height = 80; + } else if (item.componentType === "checkbox") { + width = 150; + height = 30; } // 여백을 px로 변환 (1mm ≈ 3.7795px) @@ -218,6 +221,15 @@ export function ReportDesignerCanvas() { barcodeMargin: 10, qrErrorCorrectionLevel: "M" as const, }), + // 체크박스 컴포넌트 전용 + ...(item.componentType === "checkbox" && { + checkboxChecked: false, + checkboxLabel: "항목", + checkboxSize: 18, + checkboxColor: "#2563eb", + checkboxBorderColor: "#6b7280", + checkboxLabelPosition: "right" as const, + }), }; addComponent(newComponent); diff --git a/frontend/components/report/designer/ReportDesignerRightPanel.tsx b/frontend/components/report/designer/ReportDesignerRightPanel.tsx index 58dd5047..aef6d1f2 100644 --- a/frontend/components/report/designer/ReportDesignerRightPanel.tsx +++ b/frontend/components/report/designer/ReportDesignerRightPanel.tsx @@ -1834,11 +1834,170 @@ export function ReportDesignerRightPanel() { )} - {/* 데이터 바인딩 (텍스트/라벨/테이블/바코드 컴포넌트) */} + {/* 체크박스 컴포넌트 전용 설정 */} + {selectedComponent.type === "checkbox" && ( + + + 체크박스 설정 + + + {/* 체크 상태 (쿼리 연결 없을 때) */} + {!selectedComponent.queryId && ( +
+ + updateComponent(selectedComponent.id, { + checkboxChecked: e.target.checked, + }) + } + className="h-4 w-4 rounded border-gray-300" + /> + +
+ )} + + {/* 쿼리 연결 시 필드 선택 */} + {selectedComponent.queryId && ( +
+ + +

+ true, "Y", 1 등 truthy 값이면 체크됨 +

+
+ )} + + {/* 레이블 텍스트 */} +
+ + + updateComponent(selectedComponent.id, { + checkboxLabel: e.target.value, + }) + } + placeholder="체크박스 옆 텍스트" + className="h-8" + /> +
+ + {/* 레이블 위치 */} +
+ + +
+ + {/* 체크박스 크기 */} +
+ + + updateComponent(selectedComponent.id, { + checkboxSize: Number(e.target.value), + }) + } + min={12} + max={40} + className="h-8" + /> +
+ + {/* 색상 설정 */} +
+
+ + + updateComponent(selectedComponent.id, { + checkboxColor: e.target.value, + }) + } + className="h-8 w-full" + /> +
+
+ + + updateComponent(selectedComponent.id, { + checkboxBorderColor: e.target.value, + }) + } + className="h-8 w-full" + /> +
+
+ + {/* 쿼리 연결 안내 */} + {!selectedComponent.queryId && ( +
+ 쿼리를 연결하면 데이터베이스 값으로 체크 상태를 결정할 수 있습니다. +
+ )} +
+
+ )} + + {/* 데이터 바인딩 (텍스트/라벨/테이블/바코드/체크박스 컴포넌트) */} {(selectedComponent.type === "text" || selectedComponent.type === "label" || selectedComponent.type === "table" || - selectedComponent.type === "barcode") && ( + selectedComponent.type === "barcode" || + selectedComponent.type === "checkbox") && (
diff --git a/frontend/types/report.ts b/frontend/types/report.ts index bd4c8e68..9a01638a 100644 --- a/frontend/types/report.ts +++ b/frontend/types/report.ts @@ -198,6 +198,14 @@ export interface ComponentConfig { barcodeBackground?: string; // 배경 색상 barcodeMargin?: number; // 여백 qrErrorCorrectionLevel?: "L" | "M" | "Q" | "H"; // QR 오류 보정 수준 + // 체크박스 컴포넌트 전용 + checkboxChecked?: boolean; // 체크 상태 (고정값) + checkboxFieldName?: string; // 쿼리 필드 바인딩 (truthy/falsy 값) + checkboxLabel?: string; // 체크박스 옆 레이블 텍스트 + checkboxSize?: number; // 체크박스 크기 (px) + checkboxColor?: string; // 체크 색상 + checkboxBorderColor?: string; // 테두리 색상 + checkboxLabelPosition?: "left" | "right"; // 레이블 위치 } // 리포트 상세