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

261 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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>
);
}