컴포넌트 스타일링 구현
This commit is contained in:
parent
7cefc39b74
commit
de97c40517
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { useRef, useState } from "react";
|
||||
import { useRef, useState, useEffect } from "react";
|
||||
import { ComponentConfig } from "@/types/report";
|
||||
import { useReportDesigner } from "@/contexts/ReportDesignerContext";
|
||||
|
||||
|
|
@ -46,7 +46,7 @@ export function CanvasComponent({ component }: CanvasComponentProps) {
|
|||
};
|
||||
|
||||
// 마우스 이동 핸들러 (전역)
|
||||
useState(() => {
|
||||
useEffect(() => {
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (isDragging) {
|
||||
const newX = Math.max(0, e.clientX - dragStart.x);
|
||||
|
|
@ -74,7 +74,7 @@ export function CanvasComponent({ component }: CanvasComponentProps) {
|
|||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
};
|
||||
}
|
||||
});
|
||||
}, [isDragging, isResizing, dragStart, resizeStart, component.id, updateComponent]);
|
||||
|
||||
// 표시할 값 결정
|
||||
const getDisplayValue = (): string => {
|
||||
|
|
@ -119,14 +119,17 @@ export function CanvasComponent({ component }: CanvasComponentProps) {
|
|||
<span>텍스트 필드</span>
|
||||
{hasBinding && <span className="text-blue-600">● 연결됨</span>}
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
className="w-full rounded border px-2 py-1 text-sm"
|
||||
placeholder={displayValue}
|
||||
value={displayValue}
|
||||
readOnly
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
fontSize: `${component.fontSize}px`,
|
||||
color: component.fontColor,
|
||||
fontWeight: component.fontWeight,
|
||||
textAlign: component.textAlign as "left" | "center" | "right",
|
||||
}}
|
||||
className="w-full"
|
||||
>
|
||||
{displayValue}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
@ -137,7 +140,16 @@ export function CanvasComponent({ component }: CanvasComponentProps) {
|
|||
<span>레이블</span>
|
||||
{hasBinding && <span className="text-blue-600">● 연결됨</span>}
|
||||
</div>
|
||||
<div className="font-semibold">{displayValue}</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: `${component.fontSize}px`,
|
||||
color: component.fontColor,
|
||||
fontWeight: component.fontWeight,
|
||||
textAlign: component.textAlign as "left" | "center" | "right",
|
||||
}}
|
||||
>
|
||||
{displayValue}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
@ -218,15 +230,17 @@ export function CanvasComponent({ component }: CanvasComponentProps) {
|
|||
return (
|
||||
<div
|
||||
ref={componentRef}
|
||||
className={`absolute cursor-move rounded border-2 bg-white p-2 shadow-sm ${
|
||||
isSelected ? "border-blue-500 ring-2 ring-blue-300" : "border-gray-400"
|
||||
}`}
|
||||
className={`absolute cursor-move p-2 shadow-sm ${isSelected ? "ring-2 ring-blue-500" : ""}`}
|
||||
style={{
|
||||
left: `${component.x}px`,
|
||||
top: `${component.y}px`,
|
||||
width: `${component.width}px`,
|
||||
height: `${component.height}px`,
|
||||
zIndex: component.zIndex,
|
||||
backgroundColor: component.backgroundColor,
|
||||
border: component.borderWidth
|
||||
? `${component.borderWidth}px solid ${component.borderColor}`
|
||||
: "1px solid #e5e7eb",
|
||||
}}
|
||||
onMouseDown={handleMouseDown}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { useRef } from "react";
|
||||
import { useRef, useEffect } from "react";
|
||||
import { useDrop } from "react-dnd";
|
||||
import { useReportDesigner } from "@/contexts/ReportDesignerContext";
|
||||
import { ComponentConfig } from "@/types/report";
|
||||
|
|
@ -9,7 +9,8 @@ import { v4 as uuidv4 } from "uuid";
|
|||
|
||||
export function ReportDesignerCanvas() {
|
||||
const canvasRef = useRef<HTMLDivElement>(null);
|
||||
const { components, addComponent, canvasWidth, canvasHeight, selectComponent } = useReportDesigner();
|
||||
const { components, addComponent, canvasWidth, canvasHeight, selectComponent, selectedComponentId, removeComponent } =
|
||||
useReportDesigner();
|
||||
|
||||
const [{ isOver }, drop] = useDrop(() => ({
|
||||
accept: "component",
|
||||
|
|
@ -60,6 +61,18 @@ export function ReportDesignerCanvas() {
|
|||
}
|
||||
};
|
||||
|
||||
// Delete 키 삭제 처리
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Delete" && selectedComponentId) {
|
||||
removeComponent(selectedComponentId);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [selectedComponentId, removeComponent]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 flex-col overflow-hidden bg-gray-100">
|
||||
{/* 작업 영역 제목 */}
|
||||
|
|
|
|||
|
|
@ -131,49 +131,173 @@ export function ReportDesignerRightPanel() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* 글꼴 크기 */}
|
||||
<div>
|
||||
<Label className="text-xs">글꼴 크기</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={selectedComponent.fontSize || 13}
|
||||
onChange={(e) =>
|
||||
updateComponent(selectedComponent.id, {
|
||||
fontSize: parseInt(e.target.value) || 13,
|
||||
})
|
||||
}
|
||||
className="h-8"
|
||||
/>
|
||||
</div>
|
||||
{/* 스타일링 섹션 */}
|
||||
<div className="space-y-3 rounded-md border border-gray-200 bg-gray-50 p-3">
|
||||
<h4 className="text-xs font-semibold text-gray-700">스타일</h4>
|
||||
|
||||
{/* 글꼴 색상 */}
|
||||
<div>
|
||||
<Label className="text-xs">글꼴 색상</Label>
|
||||
<Input
|
||||
type="color"
|
||||
value={selectedComponent.fontColor || "#000000"}
|
||||
onChange={(e) =>
|
||||
updateComponent(selectedComponent.id, {
|
||||
fontColor: e.target.value,
|
||||
})
|
||||
}
|
||||
className="h-8"
|
||||
/>
|
||||
</div>
|
||||
{/* 글꼴 크기 */}
|
||||
<div>
|
||||
<Label className="text-xs">글꼴 크기</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={selectedComponent.fontSize || 13}
|
||||
onChange={(e) =>
|
||||
updateComponent(selectedComponent.id, {
|
||||
fontSize: parseInt(e.target.value) || 13,
|
||||
})
|
||||
}
|
||||
className="h-8"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 배경 색상 */}
|
||||
<div>
|
||||
<Label className="text-xs">배경 색상</Label>
|
||||
<Input
|
||||
type="color"
|
||||
value={selectedComponent.backgroundColor || "#ffffff"}
|
||||
onChange={(e) =>
|
||||
updateComponent(selectedComponent.id, {
|
||||
backgroundColor: e.target.value,
|
||||
})
|
||||
}
|
||||
className="h-8"
|
||||
/>
|
||||
{/* 글꼴 색상 */}
|
||||
<div>
|
||||
<Label className="text-xs">글꼴 색상</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type="color"
|
||||
value={selectedComponent.fontColor || "#000000"}
|
||||
onChange={(e) =>
|
||||
updateComponent(selectedComponent.id, {
|
||||
fontColor: e.target.value,
|
||||
})
|
||||
}
|
||||
className="h-8 w-16"
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
value={selectedComponent.fontColor || "#000000"}
|
||||
onChange={(e) =>
|
||||
updateComponent(selectedComponent.id, {
|
||||
fontColor: e.target.value,
|
||||
})
|
||||
}
|
||||
className="h-8 flex-1 font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 텍스트 정렬 (텍스트/라벨만) */}
|
||||
{(selectedComponent.type === "text" || selectedComponent.type === "label") && (
|
||||
<div>
|
||||
<Label className="text-xs">텍스트 정렬</Label>
|
||||
<Select
|
||||
value={selectedComponent.textAlign || "left"}
|
||||
onValueChange={(value) =>
|
||||
updateComponent(selectedComponent.id, {
|
||||
textAlign: value as "left" | "center" | "right",
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-8">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="left">왼쪽</SelectItem>
|
||||
<SelectItem value="center">가운데</SelectItem>
|
||||
<SelectItem value="right">오른쪽</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 글꼴 굵기 (텍스트/라벨만) */}
|
||||
{(selectedComponent.type === "text" || selectedComponent.type === "label") && (
|
||||
<div>
|
||||
<Label className="text-xs">글꼴 굵기</Label>
|
||||
<Select
|
||||
value={selectedComponent.fontWeight || "normal"}
|
||||
onValueChange={(value) =>
|
||||
updateComponent(selectedComponent.id, {
|
||||
fontWeight: value as "normal" | "bold",
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-8">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="normal">보통</SelectItem>
|
||||
<SelectItem value="bold">굵게</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 배경 색상 */}
|
||||
<div>
|
||||
<Label className="text-xs">배경 색상</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type="color"
|
||||
value={selectedComponent.backgroundColor || "#ffffff"}
|
||||
onChange={(e) =>
|
||||
updateComponent(selectedComponent.id, {
|
||||
backgroundColor: e.target.value,
|
||||
})
|
||||
}
|
||||
className="h-8 w-16"
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
value={selectedComponent.backgroundColor || "#ffffff"}
|
||||
onChange={(e) =>
|
||||
updateComponent(selectedComponent.id, {
|
||||
backgroundColor: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="transparent"
|
||||
className="h-8 flex-1 font-mono text-xs"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
updateComponent(selectedComponent.id, {
|
||||
backgroundColor: "transparent",
|
||||
})
|
||||
}
|
||||
className="h-8 px-2 text-xs"
|
||||
>
|
||||
없음
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 테두리 */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">테두리</Label>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<Label className="text-xs text-gray-600">두께</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
max="10"
|
||||
value={selectedComponent.borderWidth || 0}
|
||||
onChange={(e) =>
|
||||
updateComponent(selectedComponent.id, {
|
||||
borderWidth: parseInt(e.target.value) || 0,
|
||||
})
|
||||
}
|
||||
className="h-8"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs text-gray-600">색상</Label>
|
||||
<Input
|
||||
type="color"
|
||||
value={selectedComponent.borderColor || "#cccccc"}
|
||||
onChange={(e) =>
|
||||
updateComponent(selectedComponent.id, {
|
||||
borderColor: e.target.value,
|
||||
})
|
||||
}
|
||||
className="h-8"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 데이터 바인딩 (텍스트/라벨/테이블 컴포넌트) */}
|
||||
|
|
|
|||
|
|
@ -104,6 +104,11 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
|
|||
top: `${component.y}px`,
|
||||
width: `${component.width}px`,
|
||||
height: `${component.height}px`,
|
||||
backgroundColor: component.backgroundColor,
|
||||
border: component.borderWidth
|
||||
? `${component.borderWidth}px solid ${component.borderColor}`
|
||||
: "none",
|
||||
padding: "8px",
|
||||
}}
|
||||
>
|
||||
{component.type === "text" && (
|
||||
|
|
@ -111,7 +116,8 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
|
|||
style={{
|
||||
fontSize: `${component.fontSize}px`,
|
||||
color: component.fontColor,
|
||||
backgroundColor: component.backgroundColor,
|
||||
fontWeight: component.fontWeight,
|
||||
textAlign: component.textAlign as "left" | "center" | "right",
|
||||
}}
|
||||
>
|
||||
{displayValue}
|
||||
|
|
@ -120,11 +126,11 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
|
|||
|
||||
{component.type === "label" && (
|
||||
<div
|
||||
className="font-semibold"
|
||||
style={{
|
||||
fontSize: `${component.fontSize}px`,
|
||||
color: component.fontColor,
|
||||
backgroundColor: component.backgroundColor,
|
||||
fontWeight: component.fontWeight,
|
||||
textAlign: component.textAlign as "left" | "center" | "right",
|
||||
}}
|
||||
>
|
||||
{displayValue}
|
||||
|
|
|
|||
Loading…
Reference in New Issue