import React, { useState, useEffect, useCallback, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Badge } from "@/components/ui/badge"; import { Plus, Trash2, GripVertical, Layers, SplitSquareVertical, ChevronDown, ChevronRight, Zap, Loader2, Box, Settings2, } from "lucide-react"; import { cn } from "@/lib/utils"; import { screenApi } from "@/lib/api/screen"; import { toast } from "sonner"; import { ComponentData, ConditionalZone } from "@/types/screen-management"; // DB 레이어 타입 interface DBLayer { layer_id: number; layer_name: string; condition_config: any; component_count: number; updated_at: string; } interface LayerManagerPanelProps { screenId: number | null; activeLayerId: number; // 현재 활성 레이어 ID (DB layer_id) onLayerChange: (layerId: number) => void; // 레이어 전환 components?: ComponentData[]; // 현재 활성 레이어의 컴포넌트 (폴백용) zones?: ConditionalZone[]; // Zone 목록 (ScreenDesigner에서 전달) onZonesChange?: (zones: ConditionalZone[]) => void; // Zone 목록 변경 콜백 } export const LayerManagerPanel: React.FC = ({ screenId, activeLayerId, onLayerChange, components = [], zones: externalZones, onZonesChange, }) => { const [layers, setLayers] = useState([]); const [isLoading, setIsLoading] = useState(false); // 펼침/접힘 상태: zone_id별 const [expandedZones, setExpandedZones] = useState>(new Set()); // Zone에 레이어 추가 시 조건값 입력 상태 const [addingToZoneId, setAddingToZoneId] = useState(null); const [newConditionValue, setNewConditionValue] = useState(""); // Zone 트리거 설정 열기 상태 const [triggerEditZoneId, setTriggerEditZoneId] = useState(null); // 기본 레이어 컴포넌트 (트리거 선택용) const [baseLayerComponents, setBaseLayerComponents] = useState([]); const zones = externalZones || []; // 레이어 목록 로드 const loadLayers = useCallback(async () => { if (!screenId) return; setIsLoading(true); try { const data = await screenApi.getScreenLayers(screenId); setLayers(data); } catch (error) { console.error("레이어 목록 로드 실패:", error); } finally { setIsLoading(false); } }, [screenId]); // 기본 레이어 컴포넌트 로드 const loadBaseLayerComponents = useCallback(async () => { if (!screenId) return; // 현재 활성 레이어가 기본 레이어(1)이면 props의 실시간 컴포넌트 사용 if (activeLayerId === 1 && components.length > 0) { setBaseLayerComponents(components); return; } try { const data = await screenApi.getLayerLayout(screenId, 1); if (data?.components) { setBaseLayerComponents(data.components as ComponentData[]); } } catch { setBaseLayerComponents(components); } }, [screenId, components, activeLayerId]); useEffect(() => { loadLayers(); loadBaseLayerComponents(); }, [loadLayers, loadBaseLayerComponents]); // Zone별 레이어 그룹핑 const getLayersForZone = useCallback((zoneId: number): DBLayer[] => { return layers.filter(l => { const cc = l.condition_config; return cc && cc.zone_id === zoneId; }); }, [layers]); // Zone에 속하지 않는 조건부 레이어 (레거시) const orphanLayers = layers.filter(l => { if (l.layer_id === 1) return false; const cc = l.condition_config; return !cc || !cc.zone_id; }); // 기본 레이어 const baseLayer = layers.find(l => l.layer_id === 1); // Zone에 레이어 추가 const handleAddLayerToZone = useCallback(async (zoneId: number) => { if (!screenId || !newConditionValue.trim()) return; try { const result = await screenApi.addLayerToZone( screenId, zoneId, newConditionValue.trim(), `레이어 (${newConditionValue.trim()})`, ); toast.success(`레이어가 Zone에 추가되었습니다. (ID: ${result.layerId})`); setAddingToZoneId(null); setNewConditionValue(""); await loadLayers(); onLayerChange(result.layerId); } catch (error) { console.error("Zone 레이어 추가 실패:", error); toast.error("레이어 추가에 실패했습니다."); } }, [screenId, newConditionValue, loadLayers, onLayerChange]); // 레이어 삭제 const handleDeleteLayer = useCallback(async (layerId: number) => { if (!screenId || layerId === 1) return; try { await screenApi.deleteLayer(screenId, layerId); toast.success("레이어가 삭제되었습니다."); await loadLayers(); if (activeLayerId === layerId) onLayerChange(1); } catch (error) { console.error("레이어 삭제 실패:", error); toast.error("레이어 삭제에 실패했습니다."); } }, [screenId, activeLayerId, loadLayers, onLayerChange]); // Zone 삭제 const handleDeleteZone = useCallback(async (zoneId: number) => { if (!screenId) return; try { await screenApi.deleteZone(zoneId); toast.success("조건부 영역이 삭제되었습니다."); // Zone 목록 새로고침 const loadedZones = await screenApi.getScreenZones(screenId); onZonesChange?.(loadedZones); await loadLayers(); onLayerChange(1); } catch (error) { console.error("Zone 삭제 실패:", error); toast.error("Zone 삭제에 실패했습니다."); } }, [screenId, loadLayers, onLayerChange, onZonesChange]); // Zone 트리거 컴포넌트 업데이트 const handleUpdateZoneTrigger = useCallback(async (zoneId: number, triggerComponentId: string, operator: string = "eq") => { try { await screenApi.updateZone(zoneId, { trigger_component_id: triggerComponentId, trigger_operator: operator, }); const loadedZones = await screenApi.getScreenZones(screenId!); onZonesChange?.(loadedZones); toast.success("트리거가 설정되었습니다."); } catch (error) { console.error("Zone 트리거 업데이트 실패:", error); toast.error("트리거 설정에 실패했습니다."); } }, [screenId, onZonesChange]); // Zone 접힘/펼침 토글 const toggleZone = (zoneId: number) => { setExpandedZones(prev => { const next = new Set(prev); next.has(zoneId) ? next.delete(zoneId) : next.add(zoneId); return next; }); }; // 트리거로 사용 가능한 컴포넌트 (select, combobox 등) const triggerableComponents = baseLayerComponents.filter(c => ["select", "combobox", "radio-group"].some(t => c.componentType?.includes(t)) ); // Zone의 트리거 컴포넌트에서 옵션 목록 가져오기 const getTriggerOptions = useCallback((zone: ConditionalZone): { value: string; label: string }[] => { if (!zone.trigger_component_id) return []; const triggerComp = baseLayerComponents.find(c => c.id === zone.trigger_component_id); if (!triggerComp) return []; const config = triggerComp.componentConfig || {}; // 정적 옵션 (v2-select static source) if (config.options && Array.isArray(config.options)) { return config.options .filter((opt: any) => opt.value) .map((opt: any) => ({ value: opt.value, label: opt.label || opt.value })); } return []; }, [baseLayerComponents]); return (
{/* 헤더 */}

레이어

{layers.length}
{/* 레이어 + Zone 목록 */}
{isLoading ? (
로딩 중...
) : ( <> {/* 기본 레이어 */} {baseLayer && (
onLayerChange(1)} >
{baseLayer.layer_name}
기본 {baseLayer.component_count}개 컴포넌트
)} {/* 조건부 영역(Zone) 목록 */} {zones.map((zone) => { const zoneLayers = getLayersForZone(zone.zone_id); const isExpanded = expandedZones.has(zone.zone_id); const isTriggerEdit = triggerEditZoneId === zone.zone_id; return (
{/* Zone 헤더 */}
toggleZone(zone.zone_id)} > {isExpanded ? ( ) : ( )}
{zone.zone_name}
Zone {zoneLayers.length}개 레이어 | {zone.width}x{zone.height} {zone.trigger_component_id && ( 트리거 )}
{/* Zone 액션 버튼 */}
{/* 펼쳐진 Zone 내용 */} {isExpanded && (
{/* 트리거 설정 패널 */} {isTriggerEdit && (

트리거 컴포넌트 선택

{triggerableComponents.length === 0 ? (

기본 레이어에 Select/Combobox/Radio 컴포넌트가 없습니다.

) : (
{triggerableComponents.map(c => ( ))}
)}
)} {/* Zone 소속 레이어 목록 */} {zoneLayers.map((layer) => { const isActive = activeLayerId === layer.layer_id; const triggerOpts = getTriggerOptions(zone); const currentCondValue = layer.condition_config?.condition_value || ""; return (
onLayerChange(layer.layer_id)} >
{layer.layer_name}
{triggerOpts.length > 0 ? ( ) : ( 조건값: {currentCondValue || "미설정"} )} | {layer.component_count}개
); })} {/* 레이어 추가 */} {addingToZoneId === zone.zone_id ? (
{(() => { const triggerOpts = getTriggerOptions(zone); // 이미 사용된 조건값 제외 const usedValues = new Set( zoneLayers.map(l => l.condition_config?.condition_value).filter(Boolean) ); const availableOpts = triggerOpts.filter(o => !usedValues.has(o.value)); if (availableOpts.length > 0) { return ( ); } return ( setNewConditionValue(e.target.value)} placeholder="조건값 입력" className="h-6 text-[11px] flex-1" autoFocus onKeyDown={(e) => { if (e.key === "Enter") handleAddLayerToZone(zone.zone_id); }} /> ); })()}
) : ( )}
)}
); })} {/* 고아 레이어 (Zone에 소속되지 않은 조건부 레이어) */} {orphanLayers.length > 0 && (

Zone 미할당 레이어

{orphanLayers.map((layer) => { const isActive = activeLayerId === layer.layer_id; return (
onLayerChange(layer.layer_id)} >
{layer.layer_name}
조건부 {layer.component_count}개
); })}
)} )}
{/* Zone 생성 드래그 영역 */}
{ e.dataTransfer.setData("application/json", JSON.stringify({ type: "create-zone" })); e.dataTransfer.effectAllowed = "copy"; }} > 조건부 영역 추가 (캔버스로 드래그)

기본 레이어에서 Zone을 배치한 후, Zone 내에 레이어를 추가하세요

); };