"use client"; import { useState, useCallback, useEffect, useMemo } from "react"; import { DndProvider } from "react-dnd"; import { HTML5Backend } from "react-dnd-html5-backend"; import { ArrowLeft, Save, Smartphone, Tablet } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { ResizableHandle, ResizablePanel, ResizablePanelGroup, } from "@/components/ui/resizable"; import { toast } from "sonner"; import { PopCanvas } from "./PopCanvas"; import { PopPanel } from "./panels/PopPanel"; import { PopLayoutDataV2, PopLayoutModeKey, PopComponentType, GridPosition, PopSectionDefinition, createEmptyPopLayoutV2, createSectionDefinition, createComponentDefinition, ensureV2Layout, addSectionToV2Layout, addComponentToV2Layout, removeSectionFromV2Layout, removeComponentFromV2Layout, updateSectionPositionInMode, updateComponentPositionInMode, isV2Layout, } from "./types/pop-layout"; import { screenApi } from "@/lib/api/screen"; import { ScreenDefinition } from "@/types/screen"; // ======================================== // 디바이스 타입 // ======================================== type DeviceType = "mobile" | "tablet"; /** * 디바이스 + 방향 → 모드 키 변환 */ const getModeKey = (device: DeviceType, isLandscape: boolean): PopLayoutModeKey => { if (device === "tablet") { return isLandscape ? "tablet_landscape" : "tablet_portrait"; } return isLandscape ? "mobile_landscape" : "mobile_portrait"; }; // ======================================== // Props // ======================================== interface PopDesignerProps { selectedScreen: ScreenDefinition; onBackToList: () => void; onScreenUpdate?: (updatedScreen: Partial) => void; } // ======================================== // 메인 컴포넌트 // ======================================== export default function PopDesigner({ selectedScreen, onBackToList, onScreenUpdate, }: PopDesignerProps) { // ======================================== // 레이아웃 상태 (v2) // ======================================== const [layout, setLayout] = useState(createEmptyPopLayoutV2()); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); const [hasChanges, setHasChanges] = useState(false); // ======================================== // 디바이스/모드 상태 // ======================================== const [activeDevice, setActiveDevice] = useState("tablet"); // 활성 모드 키 (가로/세로 중 현재 포커스된 캔버스) // 기본값: 태블릿 가로 const [activeModeKey, setActiveModeKey] = useState("tablet_landscape"); // ======================================== // 선택 상태 // ======================================== const [selectedSectionId, setSelectedSectionId] = useState(null); const [selectedComponentId, setSelectedComponentId] = useState(null); // ======================================== // 파생 상태 // ======================================== // 선택된 섹션 정의 const selectedSection: PopSectionDefinition | null = useMemo(() => { if (!selectedSectionId) return null; return layout.sections[selectedSectionId] || null; }, [layout.sections, selectedSectionId]); // 현재 활성 모드의 섹션 ID 목록 const activeSectionIds = useMemo(() => { return Object.keys(layout.layouts[activeModeKey].sectionPositions); }, [layout.layouts, activeModeKey]); // ======================================== // 레이아웃 로드 // ======================================== useEffect(() => { const loadLayout = async () => { if (!selectedScreen?.screenId) return; setIsLoading(true); try { // API가 layout_data 내용을 직접 반환 (언래핑 상태) const loadedLayout = await screenApi.getLayoutPop(selectedScreen.screenId); if (loadedLayout) { // v1 또는 v2 → v2로 변환 const v2Layout = ensureV2Layout(loadedLayout); setLayout(v2Layout); const sectionCount = Object.keys(v2Layout.sections).length; const componentCount = Object.keys(v2Layout.components).length; console.log(`POP v2 레이아웃 로드 성공: ${sectionCount}개 섹션, ${componentCount}개 컴포넌트`); // v1에서 마이그레이션된 경우 알림 if (!isV2Layout(loadedLayout)) { console.log("v1 → v2 자동 마이그레이션 완료"); } } else { // 레이아웃 없음 - 빈 v2 레이아웃 생성 console.log("POP 레이아웃 없음, 빈 v2 레이아웃 생성"); setLayout(createEmptyPopLayoutV2()); } } catch (error) { console.error("레이아웃 로드 실패:", error); toast.error("레이아웃을 불러오는데 실패했습니다"); setLayout(createEmptyPopLayoutV2()); } finally { setIsLoading(false); } }; loadLayout(); }, [selectedScreen?.screenId]); // ======================================== // 저장 // ======================================== const handleSave = useCallback(async () => { if (!selectedScreen?.screenId) return; setIsSaving(true); try { await screenApi.saveLayoutPop(selectedScreen.screenId, layout); toast.success("저장되었습니다"); setHasChanges(false); } catch (error) { console.error("저장 실패:", error); toast.error("저장에 실패했습니다"); } finally { setIsSaving(false); } }, [selectedScreen?.screenId, layout]); // ======================================== // 섹션 추가 (4모드 동기화) // ======================================== const handleDropSection = useCallback((gridPosition: GridPosition) => { const newId = `section-${Date.now()}`; setLayout((prev) => addSectionToV2Layout(prev, newId, gridPosition)); setSelectedSectionId(newId); setHasChanges(true); }, []); // ======================================== // 컴포넌트 추가 (4모드 동기화) // ======================================== const handleDropComponent = useCallback( (sectionId: string, type: PopComponentType, gridPosition: GridPosition) => { const newId = `${type}-${Date.now()}`; setLayout((prev) => addComponentToV2Layout(prev, sectionId, newId, type, gridPosition)); setSelectedComponentId(newId); setHasChanges(true); }, [] ); // ======================================== // 섹션 정의 업데이트 (공유) // ======================================== const handleUpdateSectionDefinition = useCallback( (sectionId: string, updates: Partial) => { setLayout((prev) => ({ ...prev, sections: { ...prev.sections, [sectionId]: { ...prev.sections[sectionId], ...updates, }, }, })); setHasChanges(true); }, [] ); // ======================================== // 섹션 위치 업데이트 (현재 모드만) // ======================================== const handleUpdateSectionPosition = useCallback( (sectionId: string, position: GridPosition, modeKey?: PopLayoutModeKey) => { const targetMode = modeKey || activeModeKey; setLayout((prev) => updateSectionPositionInMode(prev, targetMode, sectionId, position)); setHasChanges(true); }, [activeModeKey] ); // ======================================== // 컴포넌트 위치 업데이트 (현재 모드만) // ======================================== const handleUpdateComponentPosition = useCallback( (componentId: string, position: GridPosition, modeKey?: PopLayoutModeKey) => { const targetMode = modeKey || activeModeKey; setLayout((prev) => updateComponentPositionInMode(prev, targetMode, componentId, position)); setHasChanges(true); }, [activeModeKey] ); // ======================================== // 섹션 삭제 (4모드 동기화) // ======================================== const handleDeleteSection = useCallback((sectionId: string) => { setLayout((prev) => removeSectionFromV2Layout(prev, sectionId)); setSelectedSectionId(null); setSelectedComponentId(null); setHasChanges(true); }, []); // ======================================== // 컴포넌트 삭제 (4모드 동기화) // ======================================== const handleDeleteComponent = useCallback( (sectionId: string, componentId: string) => { setLayout((prev) => removeComponentFromV2Layout(prev, sectionId, componentId)); setSelectedComponentId(null); setHasChanges(true); }, [] ); // ======================================== // 디바이스 전환 // ======================================== const handleDeviceChange = useCallback((device: DeviceType) => { setActiveDevice(device); // 기본 모드 키 설정 (가로) setActiveModeKey(device === "tablet" ? "tablet_landscape" : "mobile_landscape"); }, []); // ======================================== // 모드 키 전환 (캔버스 포커스) // ======================================== const handleModeKeyChange = useCallback((modeKey: PopLayoutModeKey) => { setActiveModeKey(modeKey); }, []); // ======================================== // 뒤로가기 // ======================================== const handleBack = useCallback(() => { if (hasChanges) { if (confirm("저장하지 않은 변경사항이 있습니다. 나가시겠습니까?")) { onBackToList(); } } else { onBackToList(); } }, [hasChanges, onBackToList]); // ======================================== // Delete 키 삭제 기능 // ======================================== useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { // input/textarea 포커스 시 제외 const target = e.target as HTMLElement; if ( target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable ) { return; } // Delete 또는 Backspace 키 if (e.key === "Delete" || e.key === "Backspace") { e.preventDefault(); // 브라우저 뒤로가기 방지 // 컴포넌트가 선택되어 있으면 컴포넌트 삭제 if (selectedComponentId) { // v2 구조: 컴포넌트가 속한 섹션을 sections의 componentIds에서 찾기 // (PopComponentDefinition에는 sectionId가 없으므로 섹션을 순회하여 찾음) let foundSectionId: string | null = null; for (const [sectionId, sectionDef] of Object.entries(layout.sections)) { if (sectionDef.componentIds.includes(selectedComponentId)) { foundSectionId = sectionId; break; } } if (foundSectionId) { handleDeleteComponent(foundSectionId, selectedComponentId); } } // 컴포넌트가 선택되지 않았고 섹션이 선택되어 있으면 섹션 삭제 else if (selectedSectionId) { handleDeleteSection(selectedSectionId); } } }; window.addEventListener("keydown", handleKeyDown); return () => { window.removeEventListener("keydown", handleKeyDown); }; }, [ selectedComponentId, selectedSectionId, layout.sections, handleDeleteComponent, handleDeleteSection, ]); // ======================================== // 로딩 상태 // ======================================== if (isLoading) { return (
로딩 중...
); } // ======================================== // 렌더링 // ======================================== return (
{/* 툴바 */}
{/* 왼쪽: 뒤로가기 + 화면명 */}
{selectedScreen?.screenName || "POP 화면"} {hasChanges && ( *변경됨 )}
{/* 중앙: 디바이스 전환 (가로/세로 전환 버튼 제거 - 캔버스 2개 동시 표시) */}
handleDeviceChange(v as DeviceType)} > 태블릿 모바일
{/* 오른쪽: 저장 */}
{/* 메인 영역: 리사이즈 가능한 패널 */} {/* 왼쪽: 패널 (컴포넌트/편집 탭) */} {/* 오른쪽: 캔버스 (가로+세로 2개 동시 표시) */}
); }