ERP-node/frontend/components/screen/SimpleScreenDesigner.tsx

261 lines
8.3 KiB
TypeScript
Raw Normal View History

2025-09-09 14:29:04 +09:00
"use client";
import { useState, useCallback, useRef } from "react";
import {
ScreenDefinition,
ComponentData,
LayoutData,
Position,
ScreenResolution,
SCREEN_RESOLUTIONS,
} from "@/types/screen";
import { generateComponentId } from "@/lib/utils/generateId";
import { screenApi } from "@/lib/api/screen";
import { toast } from "sonner";
import { RealtimePreview } from "./RealtimePreviewDynamic";
import DesignerToolbar from "./DesignerToolbar";
interface SimpleScreenDesignerProps {
selectedScreen: ScreenDefinition | null;
onBackToList: () => void;
}
export default function SimpleScreenDesigner({ selectedScreen, onBackToList }: SimpleScreenDesignerProps) {
const [layout, setLayout] = useState<LayoutData>({
components: [],
});
const [isSaving, setIsSaving] = useState(false);
const [screenResolution, setScreenResolution] = useState<ScreenResolution>(SCREEN_RESOLUTIONS[0]);
const [selectedComponent, setSelectedComponent] = useState<ComponentData | null>(null);
// 드래그 상태
const [dragState, setDragState] = useState({
isDragging: false,
draggedComponent: null as ComponentData | null,
originalPosition: { x: 0, y: 0, z: 1 },
currentPosition: { x: 0, y: 0, z: 1 },
grabOffset: { x: 0, y: 0 },
});
const canvasRef = useRef<HTMLDivElement>(null);
// 레이아웃 저장
const handleSave = useCallback(async () => {
if (!selectedScreen?.screenId) return;
setIsSaving(true);
try {
const layoutWithResolution = {
...layout,
screenResolution: screenResolution,
};
await screenApi.saveLayout(selectedScreen.screenId, layoutWithResolution);
toast.success("화면이 저장되었습니다.");
} catch (error) {
console.error("저장 실패:", error);
toast.error("저장 중 오류가 발생했습니다.");
} finally {
setIsSaving(false);
}
}, [selectedScreen?.screenId, layout, screenResolution]);
// 컴포넌트 추가
const addComponent = useCallback((type: ComponentData["type"], position: Position) => {
const newComponent: ComponentData = {
id: generateComponentId(),
type: type,
position: position,
size: { width: 200, height: 80 },
title: `${type}`,
...(type === "widget" && {
webType: "text" as const,
label: "라벨",
placeholder: "입력하세요",
}),
} as ComponentData;
setLayout((prev) => ({
...prev,
components: [...prev.components, newComponent],
}));
}, []);
// 드래그 시작
const startDrag = useCallback((component: ComponentData, event: React.MouseEvent) => {
event.preventDefault();
const rect = canvasRef.current?.getBoundingClientRect();
if (!rect) return;
setDragState({
isDragging: true,
draggedComponent: component,
originalPosition: component.position,
currentPosition: component.position,
grabOffset: {
x: event.clientX - rect.left - component.position.x,
y: event.clientY - rect.top - component.position.y,
},
});
}, []);
// 드래그 업데이트
const updateDragPosition = useCallback(
(event: MouseEvent) => {
if (!dragState.isDragging || !dragState.draggedComponent || !canvasRef.current) return;
const rect = canvasRef.current.getBoundingClientRect();
const newPosition = {
x: Math.max(0, event.clientX - rect.left - dragState.grabOffset.x),
y: Math.max(0, event.clientY - rect.top - dragState.grabOffset.y),
z: dragState.draggedComponent.position.z || 1,
};
setDragState((prev) => ({
...prev,
currentPosition: newPosition,
}));
},
[dragState],
);
// 드래그 종료
const endDrag = useCallback(() => {
if (!dragState.isDragging || !dragState.draggedComponent) return;
const updatedComponents = layout.components.map((comp) =>
comp.id === dragState.draggedComponent!.id ? { ...comp, position: dragState.currentPosition } : comp,
);
setLayout((prev) => ({
...prev,
components: updatedComponents,
}));
setDragState({
isDragging: false,
draggedComponent: null,
originalPosition: { x: 0, y: 0, z: 1 },
currentPosition: { x: 0, y: 0, z: 1 },
grabOffset: { x: 0, y: 0 },
});
}, [dragState, layout.components]);
// 마우스 이벤트 리스너
const handleMouseMove = useCallback(
(event: MouseEvent) => {
updateDragPosition(event);
},
[updateDragPosition],
);
const handleMouseUp = useCallback(() => {
endDrag();
}, [endDrag]);
// 이벤트 리스너 등록
React.useEffect(() => {
if (dragState.isDragging) {
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
return () => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
}
}, [dragState.isDragging, handleMouseMove, handleMouseUp]);
// 캔버스 클릭 처리
const handleCanvasClick = useCallback((e: React.MouseEvent) => {
if (e.target === e.currentTarget) {
setSelectedComponent(null);
}
}, []);
// 툴바 액션들
const handleAddText = () => addComponent("widget", { x: 50, y: 50, z: 1 });
const handleAddContainer = () => addComponent("container", { x: 100, y: 100, z: 1 });
if (!selectedScreen) {
return (
<div className="flex h-screen items-center justify-center">
<p> .</p>
</div>
);
}
return (
<div className="flex h-screen w-full flex-col bg-gray-100">
{/* 상단 툴바 */}
<DesignerToolbar
screenName={selectedScreen?.screenName}
tableName={selectedScreen?.tableName}
onBack={onBackToList}
onSave={handleSave}
onUndo={() => {}}
onRedo={() => {}}
onPreview={() => toast.info("미리보기 기능은 준비 중입니다.")}
onTogglePanel={() => {}}
panelStates={{}}
canUndo={false}
canRedo={false}
isSaving={isSaving}
/>
{/* 간단한 컨트롤 버튼들 */}
<div className="border-b border-gray-300 bg-white p-4">
<div className="flex gap-2">
<button onClick={handleAddText} className="rounded bg-blue-500 px-3 py-1 text-white hover:bg-blue-600">
</button>
<button onClick={handleAddContainer} className="rounded bg-green-500 px-3 py-1 text-white hover:bg-green-600">
</button>
</div>
</div>
{/* 메인 캔버스 영역 */}
<div className="relative flex-1 overflow-auto bg-gray-100 p-8">
{/* 해상도 정보 표시 */}
<div className="mb-4 flex items-center justify-center">
<div className="rounded-lg border bg-white px-4 py-2 shadow-sm">
<span className="text-sm font-medium text-gray-700">
{screenResolution.name} ({screenResolution.width} × {screenResolution.height})
</span>
</div>
</div>
{/* 실제 작업 캔버스 */}
<div
className="mx-auto bg-white shadow-lg"
style={{ width: screenResolution.width, height: screenResolution.height }}
>
<div ref={canvasRef} className="relative h-full w-full overflow-hidden bg-white" onClick={handleCanvasClick}>
{/* 컴포넌트들 */}
{layout.components.map((component) => (
<RealtimePreview
key={component.id}
component={component}
isSelected={selectedComponent?.id === component.id}
onSelect={(comp) => setSelectedComponent(comp)}
onStartDrag={(comp, event) => startDrag(comp, event)}
onUpdateComponent={(updates) => {
setLayout((prev) => ({
...prev,
components: prev.components.map((c) => (c.id === component.id ? { ...c, ...updates } : c)),
}));
}}
dragPosition={
dragState.isDragging && dragState.draggedComponent?.id === component.id
? dragState.currentPosition
: undefined
}
hideLabel={false}
/>
))}
</div>
</div>
</div>
</div>
);
}