"use client"; import { useState, useRef } from "react"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Input } from "@/components/ui/input"; 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 { useReportDesigner } from "@/contexts/ReportDesignerContext"; import { QueryManager } from "./QueryManager"; import { SignaturePad } from "./SignaturePad"; import { reportApi } from "@/lib/api/reportApi"; import { useToast } from "@/hooks/use-toast"; export function ReportDesignerRightPanel() { const context = useReportDesigner(); const { selectedComponentId, components, updateComponent, removeComponent, queries, currentPage, currentPageId, updatePageSettings, } = context; const [activeTab, setActiveTab] = useState("properties"); const [uploadingImage, setUploadingImage] = useState(false); const fileInputRef = useRef(null); const { toast } = useToast(); const selectedComponent = components.find((c) => c.id === selectedComponentId); // 이미지 업로드 핸들러 const handleImageUpload = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file || !selectedComponent) return; // 파일 타입 체크 if (!file.type.startsWith("image/")) { toast({ title: "오류", description: "이미지 파일만 업로드 가능합니다.", variant: "destructive", }); return; } // 파일 크기 체크 (10MB) if (file.size > 10 * 1024 * 1024) { toast({ title: "오류", description: "파일 크기는 10MB 이하여야 합니다.", variant: "destructive", }); return; } try { setUploadingImage(true); const result = await reportApi.uploadImage(file); if (result.success) { // 업로드된 이미지 URL을 컴포넌트에 설정 updateComponent(selectedComponent.id, { imageUrl: result.data.fileUrl, }); toast({ title: "성공", description: "이미지가 업로드되었습니다.", }); } } catch (error) { toast({ title: "오류", description: "이미지 업로드 중 오류가 발생했습니다.", variant: "destructive", }); } finally { setUploadingImage(false); // input 초기화 if (fileInputRef.current) { fileInputRef.current.value = ""; } } }; // 선택된 쿼리의 결과 필드 가져오기 const getQueryFields = (queryId: string): string[] => { const result = context.getQueryResult(queryId); return result ? result.fields : []; }; return (
페이지 속성 쿼리
{/* 속성 탭 */}
{!selectedComponent ? (
컴포넌트를 선택하면 속성을 편집할 수 있습니다
) : (
컴포넌트 속성
{/* 타입 */}
{selectedComponent.type}
{/* 위치 */}
updateComponent(selectedComponent.id, { x: parseInt(e.target.value) || 0, }) } className="h-8" />
updateComponent(selectedComponent.id, { y: parseInt(e.target.value) || 0, }) } className="h-8" />
{/* 크기 */}
updateComponent(selectedComponent.id, { width: parseInt(e.target.value) || 50, }) } className="h-8" />
updateComponent(selectedComponent.id, { height: parseInt(e.target.value) || 30, }) } className="h-8" />
{/* 스타일링 섹션 */}

스타일

{/* 글꼴 크기 */}
updateComponent(selectedComponent.id, { fontSize: parseInt(e.target.value) || 13, }) } className="h-8" />
{/* 글꼴 색상 */}
updateComponent(selectedComponent.id, { fontColor: e.target.value, }) } className="h-8 w-16" /> updateComponent(selectedComponent.id, { fontColor: e.target.value, }) } className="h-8 flex-1 font-mono text-xs" />
{/* 텍스트 정렬 (텍스트/라벨만) */} {(selectedComponent.type === "text" || selectedComponent.type === "label") && (
)} {/* 글꼴 굵기 (텍스트/라벨만) */} {(selectedComponent.type === "text" || selectedComponent.type === "label") && (
)} {/* 배경 색상 */}
updateComponent(selectedComponent.id, { backgroundColor: e.target.value, }) } className="h-8 w-16" /> updateComponent(selectedComponent.id, { backgroundColor: e.target.value, }) } placeholder="transparent" className="h-8 flex-1 font-mono text-xs" />
{/* 테두리 */}
updateComponent(selectedComponent.id, { borderWidth: parseInt(e.target.value) || 0, }) } className="h-8" />
updateComponent(selectedComponent.id, { borderColor: e.target.value, }) } className="h-8" />
{/* 테이블 스타일 */} {selectedComponent.type === "table" && ( 테이블 스타일 {/* 헤더 배경색 */}
updateComponent(selectedComponent.id, { headerBackgroundColor: e.target.value, }) } className="h-8 w-16" /> updateComponent(selectedComponent.id, { headerBackgroundColor: e.target.value, }) } className="h-8 flex-1 font-mono text-xs" />
{/* 헤더 텍스트 색상 */}
updateComponent(selectedComponent.id, { headerTextColor: e.target.value, }) } className="h-8 w-16" /> updateComponent(selectedComponent.id, { headerTextColor: e.target.value, }) } className="h-8 flex-1 font-mono text-xs" />
{/* 테두리 표시 */}
updateComponent(selectedComponent.id, { showBorder: e.target.checked, }) } className="h-4 w-4" />
{/* 행 높이 */}
updateComponent(selectedComponent.id, { rowHeight: parseInt(e.target.value), }) } className="h-8" />
)} {/* 이미지 속성 */} {selectedComponent.type === "image" && ( 이미지 설정 {/* 파일 업로드 */}

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

{selectedComponent.imageUrl && (

현재: {selectedComponent.imageUrl}

)}
)} {/* 구분선 속성 */} {selectedComponent.type === "divider" && ( 구분선 설정
updateComponent(selectedComponent.id, { lineWidth: Number(e.target.value), }) } className="h-8" />
updateComponent(selectedComponent.id, { lineColor: e.target.value, }) } className="h-8 w-16" /> updateComponent(selectedComponent.id, { lineColor: e.target.value, }) } className="h-8 flex-1 font-mono text-xs" />
)} {/* 서명/도장 속성 */} {(selectedComponent.type === "signature" || selectedComponent.type === "stamp") && ( {selectedComponent.type === "signature" ? "서명란 설정" : "도장란 설정"} {/* 서명란: 탭으로 직접 서명 / 이미지 업로드 선택 */} {selectedComponent.type === "signature" ? ( 직접 서명 이미지 업로드 {/* 직접 서명 탭 */} { updateComponent(selectedComponent.id, { imageUrl: dataUrl, }); }} /> {/* 이미지 업로드 탭 */}

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

{selectedComponent.imageUrl && !selectedComponent.imageUrl.startsWith("data:") && (

현재: {selectedComponent.imageUrl}

)}
) : ( // 도장란: 기존 방식 유지

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

{selectedComponent.imageUrl && !selectedComponent.imageUrl.startsWith("data:") && (

현재: {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" || selectedComponent.type === "table") && (
데이터 바인딩
{/* 쿼리 선택 */}
{/* 필드 선택 (텍스트/라벨만) */} {selectedComponent.queryId && (selectedComponent.type === "text" || selectedComponent.type === "label") && (
)} {/* 테이블 컬럼 설정 */} {selectedComponent.queryId && selectedComponent.type === "table" && ( 컬럼 설정 {selectedComponent.tableColumns && selectedComponent.tableColumns.length > 0 && (
{selectedComponent.tableColumns.map((col, idx) => (
컬럼 {idx + 1}
{ const newColumns = [...selectedComponent.tableColumns!]; newColumns[idx].field = e.target.value; updateComponent(selectedComponent.id, { tableColumns: newColumns, }); }} className="h-7 text-xs" />
{ const newColumns = [...selectedComponent.tableColumns!]; newColumns[idx].header = e.target.value; updateComponent(selectedComponent.id, { tableColumns: newColumns, }); }} className="h-7 text-xs" />
{ const newColumns = [...selectedComponent.tableColumns!]; newColumns[idx].width = e.target.value ? parseInt(e.target.value) : undefined; updateComponent(selectedComponent.id, { tableColumns: newColumns, }); }} placeholder="자동" className="h-7 text-xs" />
))}
)}
)} {/* 기본값 (텍스트/라벨만) */} {(selectedComponent.type === "text" || selectedComponent.type === "label") && (
updateComponent(selectedComponent.id, { defaultValue: e.target.value, }) } placeholder="데이터가 없을 때 표시할 값" className="h-8" />
)} {/* 포맷 (텍스트/라벨만) */} {(selectedComponent.type === "text" || selectedComponent.type === "label") && (
updateComponent(selectedComponent.id, { format: e.target.value, }) } placeholder="예: YYYY-MM-DD, #,###" className="h-8" />
)}
)}
)}
{/* 페이지 설정 탭 */}
{currentPage && currentPageId ? ( <> {/* 페이지 정보 */} 페이지 정보
updatePageSettings(currentPageId, { page_name: e.target.value, }) } className="mt-1" />
{/* 페이지 크기 */} 페이지 크기
updatePageSettings(currentPageId, { width: Number(e.target.value), }) } className="mt-1" />
updatePageSettings(currentPageId, { height: Number(e.target.value), }) } className="mt-1" />
{/* 프리셋 버튼 */}
{/* 여백 설정 */} 여백 (mm)
updatePageSettings(currentPageId, { margins: { ...currentPage.margins, top: Number(e.target.value), }, }) } className="mt-1" />
updatePageSettings(currentPageId, { margins: { ...currentPage.margins, bottom: Number(e.target.value), }, }) } className="mt-1" />
updatePageSettings(currentPageId, { margins: { ...currentPage.margins, left: Number(e.target.value), }, }) } className="mt-1" />
updatePageSettings(currentPageId, { margins: { ...currentPage.margins, right: Number(e.target.value), }, }) } className="mt-1" />
{/* 여백 프리셋 */}
{/* 배경색 */} 배경
updatePageSettings(currentPageId, { background_color: e.target.value, }) } className="h-10 w-20" /> updatePageSettings(currentPageId, { background_color: e.target.value, }) } className="flex-1" placeholder="#ffffff" />
{/* 배경색 프리셋 */}
{["#ffffff", "#f3f4f6", "#e5e7eb", "#d1d5db"].map((color) => (
) : (

페이지를 선택하세요

)}
{/* 쿼리 탭 */}
); }