컴포넌트 스타일링 구현

This commit is contained in:
dohyeons 2025-10-01 14:14:06 +09:00
parent 7cefc39b74
commit de97c40517
4 changed files with 218 additions and 61 deletions

View File

@ -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}
>

View File

@ -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">
{/* 작업 영역 제목 */}

View File

@ -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>
{/* 데이터 바인딩 (텍스트/라벨/테이블 컴포넌트) */}

View File

@ -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}