From dfa642798ee19c932a7a707c3b424c41dbaa415d Mon Sep 17 00:00:00 2001 From: dohyeons Date: Wed, 1 Oct 2025 17:31:15 +0900 Subject: [PATCH] =?UTF-8?q?=EB=8F=84=EC=9E=A5,=20=EC=84=9C=EB=AA=85=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../report/designer/CanvasComponent.tsx | 112 +++++++++++ .../report/designer/ComponentPalette.tsx | 4 +- .../report/designer/ReportDesignerCanvas.tsx | 28 +++ .../designer/ReportDesignerRightPanel.tsx | 182 ++++++++++++++++++ .../report/designer/ReportPreviewModal.tsx | 117 +++++++++++ frontend/types/report.ts | 8 +- 6 files changed, 449 insertions(+), 2 deletions(-) diff --git a/frontend/components/report/designer/CanvasComponent.tsx b/frontend/components/report/designer/CanvasComponent.tsx index 1c01e31c..ce751eec 100644 --- a/frontend/components/report/designer/CanvasComponent.tsx +++ b/frontend/components/report/designer/CanvasComponent.tsx @@ -383,6 +383,118 @@ export function CanvasComponent({ component }: CanvasComponentProps) { ); + case "signature": + const sigLabelPos = component.labelPosition || "left"; + const sigShowLabel = component.showLabel !== false; + const sigLabelText = component.labelText || "서명:"; + const sigShowUnderline = component.showUnderline !== false; + + return ( +
+
서명란
+
+ {sigShowLabel && ( +
+ {sigLabelText} +
+ )} +
+ {component.imageUrl ? ( + 서명 + ) : ( +
+ 서명 이미지 +
+ )} + {sigShowUnderline && ( +
+ )} +
+
+
+ ); + + case "stamp": + const stampShowLabel = component.showLabel !== false; + const stampLabelText = component.labelText || "(인)"; + const stampPersonName = component.personName || ""; + + return ( +
+
도장란
+
+ {stampPersonName &&
{stampPersonName}
} +
+ {component.imageUrl ? ( + 도장 + ) : ( +
+ 도장 이미지 +
+ )} + {stampShowLabel && ( +
+ {stampLabelText} +
+ )} +
+
+
+ ); + default: return
알 수 없는 컴포넌트
; } diff --git a/frontend/components/report/designer/ComponentPalette.tsx b/frontend/components/report/designer/ComponentPalette.tsx index b0e251d2..d6591d0e 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, Tag, Image, Minus } from "lucide-react"; +import { Type, Table, Tag, Image, Minus, PenLine, Stamp as StampIcon } from "lucide-react"; interface ComponentItem { type: string; @@ -15,6 +15,8 @@ const COMPONENTS: ComponentItem[] = [ { type: "label", label: "레이블", icon: }, { type: "image", label: "이미지", icon: }, { type: "divider", label: "구분선", icon: }, + { type: "signature", label: "서명란", icon: }, + { type: "stamp", 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 e90a8a4b..1e690148 100644 --- a/frontend/components/report/designer/ReportDesignerCanvas.tsx +++ b/frontend/components/report/designer/ReportDesignerCanvas.tsx @@ -56,6 +56,12 @@ export function ReportDesignerCanvas() { } else if (item.componentType === "divider") { width = 300; height = 2; + } else if (item.componentType === "signature") { + width = 120; + height = 70; + } else if (item.componentType === "stamp") { + width = 70; + height = 70; } // 새 컴포넌트 생성 (Grid Snap 적용) @@ -91,6 +97,28 @@ export function ReportDesignerCanvas() { lineWidth: 1, lineColor: "#000000", }), + // 서명란 전용 + ...(item.componentType === "signature" && { + imageUrl: "", + objectFit: "contain" as const, + showLabel: true, + labelText: "서명:", + labelPosition: "left" as const, + showUnderline: true, + borderWidth: 1, + borderColor: "#cccccc", + }), + // 도장란 전용 + ...(item.componentType === "stamp" && { + imageUrl: "", + objectFit: "contain" as const, + showLabel: true, + labelText: "(인)", + labelPosition: "top" as const, + personName: "", + borderWidth: 1, + borderColor: "#cccccc", + }), }; addComponent(newComponent); diff --git a/frontend/components/report/designer/ReportDesignerRightPanel.tsx b/frontend/components/report/designer/ReportDesignerRightPanel.tsx index c84d4ff9..2aa0ecc9 100644 --- a/frontend/components/report/designer/ReportDesignerRightPanel.tsx +++ b/frontend/components/report/designer/ReportDesignerRightPanel.tsx @@ -528,6 +528,188 @@ export function ReportDesignerRightPanel() { )} + {/* 서명/도장 속성 */} + {(selectedComponent.type === "signature" || selectedComponent.type === "stamp") && ( + + + + {selectedComponent.type === "signature" ? "서명란 설정" : "도장란 설정"} + + + + {/* 파일 업로드 */} +
+ +
+ + +
+

JPG, PNG, GIF, WEBP (최대 10MB)

+ {selectedComponent.imageUrl && ( +

+ 현재: {selectedComponent.imageUrl} +

+ )} +
+ + {/* 맞춤 방식 */} +
+ + +
+ + {/* 레이블 표시 */} +
+ + updateComponent(selectedComponent.id, { + showLabel: e.target.checked, + }) + } + className="h-4 w-4" + /> + +
+ + {/* 레이블 텍스트 */} + {selectedComponent.showLabel !== false && ( + <> +
+ + + updateComponent(selectedComponent.id, { + labelText: e.target.value, + }) + } + className="h-8" + /> +
+ + {/* 레이블 위치 (서명란만) */} + {selectedComponent.type === "signature" && ( +
+ + +
+ )} + + )} + + {/* 밑줄 표시 (서명란만) */} + {selectedComponent.type === "signature" && ( +
+ + updateComponent(selectedComponent.id, { + showUnderline: e.target.checked, + }) + } + className="h-4 w-4" + /> + +
+ )} + + {/* 이름 입력 (도장란만) */} + {selectedComponent.type === "stamp" && ( +
+ + + updateComponent(selectedComponent.id, { + personName: e.target.value, + }) + } + placeholder="예: 홍길동" + className="h-8" + /> +

도장 옆에 표시될 이름

+
+ )} +
+
+ )} + {/* 데이터 바인딩 (텍스트/라벨/테이블 컴포넌트) */} {(selectedComponent.type === "text" || selectedComponent.type === "label" || diff --git a/frontend/components/report/designer/ReportPreviewModal.tsx b/frontend/components/report/designer/ReportPreviewModal.tsx index bb30a6f3..b15e60ca 100644 --- a/frontend/components/report/designer/ReportPreviewModal.tsx +++ b/frontend/components/report/designer/ReportPreviewModal.tsx @@ -370,6 +370,123 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps) }} /> )} + + {component.type === "signature" && ( +
+ {component.showLabel !== false && ( +
+ {component.labelText || "서명:"} +
+ )} +
+ {component.imageUrl && ( + 서명 + )} + {component.showUnderline !== false && ( +
+ )} +
+
+ )} + + {component.type === "stamp" && ( +
+ {component.personName && ( +
+ {component.personName} +
+ )} +
+ {component.imageUrl && ( + 도장 + )} + {component.showLabel !== false && ( +
+ {component.labelText || "(인)"} +
+ )} +
+
+ )}
); })} diff --git a/frontend/types/report.ts b/frontend/types/report.ts index 56a496b6..116ab9a9 100644 --- a/frontend/types/report.ts +++ b/frontend/types/report.ts @@ -83,7 +83,7 @@ export interface ExternalConnection { // 컴포넌트 설정 export interface ComponentConfig { id: string; - type: string; + type: string; // "text", "label", "table", "image", "divider", "signature", "stamp" x: number; y: number; width: number; @@ -117,6 +117,12 @@ export interface ComponentConfig { lineStyle?: "solid" | "dashed" | "dotted" | "double"; // 선 스타일 lineWidth?: number; // 구분선 두께 (borderWidth와 별도) lineColor?: string; // 구분선 색상 (borderColor와 별도) + // 서명/도장 전용 + showLabel?: boolean; // 레이블 표시 여부 ("서명:", "(인)") + labelText?: string; // 커스텀 레이블 텍스트 + labelPosition?: "top" | "left" | "bottom" | "right"; // 레이블 위치 + showUnderline?: boolean; // 서명란 밑줄 표시 여부 + personName?: string; // 도장란 이름 (예: "홍길동") } // 리포트 상세