"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 { reportApi } from "@/lib/api/reportApi"; import { useToast } from "@/hooks/use-toast"; export function ReportDesignerRightPanel() { const context = useReportDesigner(); const { selectedComponentId, components, updateComponent, removeComponent, queries } = 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 === "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 === "text" || selectedComponent.type === "label" || selectedComponent.type === "table") && (
데이터 바인딩
{/* 쿼리 선택 */}
{/* 필드 선택 (텍스트/라벨만) */} {selectedComponent.queryId && (selectedComponent.type === "text" || selectedComponent.type === "label") && (
)} {/* 테이블 안내 메시지 */} {selectedComponent.queryId && selectedComponent.type === "table" && (
테이블은 선택한 쿼리의 모든 필드를 자동으로 표시합니다.
)} {/* 기본값 (텍스트/라벨만) */} {(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" />
)}
)}
)}
{/* 쿼리 탭 */}
); }