feat: 레이어 시스템 추가 및 관리 기능 구현
- InteractiveScreenViewer 컴포넌트에 레이어 시스템을 도입하여, 레이어의 활성화 및 조건부 표시 로직을 추가하였습니다. - ScreenDesigner 컴포넌트에서 레이어 상태 관리 및 레이어 정보 저장 기능을 구현하였습니다. - 레이어 정의 및 조건부 표시 설정을 위한 새로운 타입과 스키마를 추가하여, 레이어 기반의 UI 구성 요소를 보다 유연하게 관리할 수 있도록 하였습니다. - 레이어별 컴포넌트 렌더링 로직을 추가하여, 모달 및 드로어 형태의 레이어를 효과적으로 처리할 수 있도록 개선하였습니다. - 전반적으로 레이어 시스템을 통해 사용자 경험을 향상시키고, UI 구성의 유연성을 높였습니다.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useCallback } from "react";
|
||||
import React, { useState, useCallback, useEffect, useMemo } from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -16,7 +16,7 @@ import { useAuth } from "@/hooks/useAuth";
|
||||
import { uploadFilesAndCreateData } from "@/lib/api/file";
|
||||
import { toast } from "sonner";
|
||||
import { useCascadingDropdown } from "@/hooks/useCascadingDropdown";
|
||||
import { CascadingDropdownConfig } from "@/types/screen-management";
|
||||
import { CascadingDropdownConfig, LayerDefinition } from "@/types/screen-management";
|
||||
import {
|
||||
ComponentData,
|
||||
WidgetComponent,
|
||||
@@ -164,6 +164,8 @@ interface InteractiveScreenViewerProps {
|
||||
enableAutoSave?: boolean;
|
||||
showToastMessages?: boolean;
|
||||
};
|
||||
// 🆕 레이어 시스템 지원
|
||||
layers?: LayerDefinition[];
|
||||
}
|
||||
|
||||
export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = ({
|
||||
@@ -178,6 +180,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
tableColumns = [],
|
||||
showValidationPanel = false,
|
||||
validationOptions = {},
|
||||
layers = [], // 🆕 레이어 목록
|
||||
}) => {
|
||||
// component가 없으면 빈 div 반환
|
||||
if (!component) {
|
||||
@@ -206,9 +209,71 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
// 팝업 전용 formData 상태
|
||||
const [popupFormData, setPopupFormData] = useState<Record<string, any>>({});
|
||||
|
||||
// 🆕 레이어 상태 관리 (런타임용)
|
||||
const [activeLayerIds, setActiveLayerIds] = useState<string[]>([]);
|
||||
|
||||
// 🆕 초기 레이어 설정 (visible인 레이어들)
|
||||
useEffect(() => {
|
||||
if (layers.length > 0) {
|
||||
const initialActiveLayers = layers.filter((l) => l.isVisible).map((l) => l.id);
|
||||
setActiveLayerIds(initialActiveLayers);
|
||||
}
|
||||
}, [layers]);
|
||||
|
||||
// 🆕 레이어 제어 액션 핸들러
|
||||
const handleLayerAction = useCallback((action: string, layerId: string) => {
|
||||
setActiveLayerIds((prev) => {
|
||||
switch (action) {
|
||||
case "show":
|
||||
return [...new Set([...prev, layerId])];
|
||||
case "hide":
|
||||
return prev.filter((id) => id !== layerId);
|
||||
case "toggle":
|
||||
return prev.includes(layerId)
|
||||
? prev.filter((id) => id !== layerId)
|
||||
: [...prev, layerId];
|
||||
case "exclusive":
|
||||
// 해당 레이어만 표시 (모달/드로어 같은 특수 레이어 처리에 활용)
|
||||
return [...prev, layerId];
|
||||
default:
|
||||
return prev;
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 통합된 폼 데이터
|
||||
const finalFormData = { ...localFormData, ...externalFormData };
|
||||
|
||||
// 🆕 조건부 레이어 로직 (formData 변경 시 자동 평가)
|
||||
useEffect(() => {
|
||||
layers.forEach((layer) => {
|
||||
if (layer.type === "conditional" && layer.condition) {
|
||||
const { targetComponentId, operator, value } = layer.condition;
|
||||
// 컴포넌트 ID를 키로 데이터 조회 - columnName 매핑이 필요할 수 있음
|
||||
const targetValue = finalFormData[targetComponentId];
|
||||
|
||||
let isMatch = false;
|
||||
switch (operator) {
|
||||
case "eq":
|
||||
isMatch = targetValue == value;
|
||||
break;
|
||||
case "neq":
|
||||
isMatch = targetValue != value;
|
||||
break;
|
||||
case "in":
|
||||
isMatch = Array.isArray(value) && value.includes(targetValue);
|
||||
break;
|
||||
}
|
||||
|
||||
if (isMatch) {
|
||||
handleLayerAction("show", layer.id);
|
||||
} else {
|
||||
handleLayerAction("hide", layer.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [finalFormData, layers, handleLayerAction]);
|
||||
|
||||
// 개선된 검증 시스템 (선택적 활성화)
|
||||
const enhancedValidation = enableEnhancedValidation && screenInfo && tableColumns.length > 0
|
||||
? useFormValidation(
|
||||
@@ -1395,7 +1460,6 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
>
|
||||
<SelectTrigger
|
||||
className="w-full"
|
||||
style={{ height: "100%" }}
|
||||
style={{
|
||||
...comp.style,
|
||||
width: "100%",
|
||||
@@ -1413,7 +1477,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>,
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2124,6 +2188,159 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
}
|
||||
: component;
|
||||
|
||||
// 🆕 레이어별 컴포넌트 렌더링 함수
|
||||
const renderLayerComponents = useCallback((layer: LayerDefinition) => {
|
||||
// 활성화되지 않은 레이어는 렌더링하지 않음
|
||||
if (!activeLayerIds.includes(layer.id)) return null;
|
||||
|
||||
// 모달 레이어 처리
|
||||
if (layer.type === "modal") {
|
||||
const modalStyle: React.CSSProperties = {
|
||||
...(layer.overlayConfig?.backgroundColor && { backgroundColor: layer.overlayConfig.backgroundColor }),
|
||||
...(layer.overlayConfig?.backdropBlur && { backdropFilter: `blur(${layer.overlayConfig.backdropBlur}px)` }),
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog key={layer.id} open={true} onOpenChange={() => handleLayerAction("hide", layer.id)}>
|
||||
<DialogContent
|
||||
className="max-w-4xl max-h-[90vh] overflow-hidden"
|
||||
style={modalStyle}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{layer.name}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="relative h-full w-full min-h-[300px]">
|
||||
{layer.components.map((comp) => (
|
||||
<div
|
||||
key={comp.id}
|
||||
className="absolute"
|
||||
style={{
|
||||
left: `${comp.position.x}px`,
|
||||
top: `${comp.position.y}px`,
|
||||
width: comp.style?.width || `${comp.size.width}px`,
|
||||
height: comp.style?.height || `${comp.size.height}px`,
|
||||
zIndex: comp.position.z || 1,
|
||||
}}
|
||||
>
|
||||
<InteractiveScreenViewer
|
||||
component={comp}
|
||||
allComponents={layer.components}
|
||||
formData={externalFormData}
|
||||
onFormDataChange={onFormDataChange}
|
||||
screenInfo={screenInfo}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
// 드로어 레이어 처리
|
||||
if (layer.type === "drawer") {
|
||||
const drawerPosition = layer.overlayConfig?.position || "right";
|
||||
const drawerWidth = layer.overlayConfig?.width || "400px";
|
||||
const drawerHeight = layer.overlayConfig?.height || "100%";
|
||||
|
||||
const drawerPositionStyles: Record<string, React.CSSProperties> = {
|
||||
right: { right: 0, top: 0, width: drawerWidth, height: "100%" },
|
||||
left: { left: 0, top: 0, width: drawerWidth, height: "100%" },
|
||||
bottom: { bottom: 0, left: 0, width: "100%", height: drawerHeight },
|
||||
top: { top: 0, left: 0, width: "100%", height: drawerHeight },
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={layer.id}
|
||||
className="fixed inset-0 z-50"
|
||||
onClick={() => handleLayerAction("hide", layer.id)}
|
||||
>
|
||||
{/* 백드롭 */}
|
||||
<div
|
||||
className="absolute inset-0 bg-black/50"
|
||||
style={{
|
||||
...(layer.overlayConfig?.backdropBlur && {
|
||||
backdropFilter: `blur(${layer.overlayConfig.backdropBlur}px)`
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
{/* 드로어 패널 */}
|
||||
<div
|
||||
className="absolute bg-background shadow-lg"
|
||||
style={drawerPositionStyles[drawerPosition]}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between border-b p-4">
|
||||
<h3 className="text-lg font-semibold">{layer.name}</h3>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleLayerAction("hide", layer.id)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="relative h-full overflow-auto p-4">
|
||||
{layer.components.map((comp) => (
|
||||
<div
|
||||
key={comp.id}
|
||||
className="absolute"
|
||||
style={{
|
||||
left: `${comp.position.x}px`,
|
||||
top: `${comp.position.y}px`,
|
||||
width: comp.style?.width || `${comp.size.width}px`,
|
||||
height: comp.style?.height || `${comp.size.height}px`,
|
||||
zIndex: comp.position.z || 1,
|
||||
}}
|
||||
>
|
||||
<InteractiveScreenViewer
|
||||
component={comp}
|
||||
allComponents={layer.components}
|
||||
formData={externalFormData}
|
||||
onFormDataChange={onFormDataChange}
|
||||
screenInfo={screenInfo}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 일반/조건부 레이어 (base, conditional)
|
||||
return (
|
||||
<div
|
||||
key={layer.id}
|
||||
className="pointer-events-none absolute inset-0"
|
||||
style={{ zIndex: layer.zIndex }}
|
||||
>
|
||||
{layer.components.map((comp) => (
|
||||
<div
|
||||
key={comp.id}
|
||||
className="pointer-events-auto absolute"
|
||||
style={{
|
||||
left: `${comp.position.x}px`,
|
||||
top: `${comp.position.y}px`,
|
||||
width: comp.style?.width || `${comp.size.width}px`,
|
||||
height: comp.style?.height || `${comp.size.height}px`,
|
||||
zIndex: comp.position.z || 1,
|
||||
}}
|
||||
>
|
||||
<InteractiveScreenViewer
|
||||
component={comp}
|
||||
allComponents={layer.components}
|
||||
formData={externalFormData}
|
||||
onFormDataChange={onFormDataChange}
|
||||
screenInfo={screenInfo}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}, [activeLayerIds, handleLayerAction, externalFormData, onFormDataChange, screenInfo]);
|
||||
|
||||
return (
|
||||
<SplitPanelProvider>
|
||||
<ActiveTabProvider>
|
||||
@@ -2147,6 +2364,9 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 🆕 레이어 렌더링 */}
|
||||
{layers.length > 0 && layers.map(renderLayerComponents)}
|
||||
|
||||
{/* 개선된 검증 패널 (선택적 표시) */}
|
||||
{showValidationPanel && enhancedValidation && (
|
||||
<div className="absolute bottom-4 right-4 z-50">
|
||||
|
||||
Reference in New Issue
Block a user