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