261 lines
8.3 KiB
TypeScript
261 lines
8.3 KiB
TypeScript
|
|
"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>
|
|||
|
|
);
|
|||
|
|
}
|