feat: Enhance LayerManagerPanel with dynamic trigger options and improved condition handling
- Added a new Select component to allow users to choose condition values dynamically based on the selected zone's trigger component. - Implemented logic to fetch trigger options from the base layer components, ensuring only relevant options are displayed. - Updated the LayerManagerPanel to handle condition values more effectively, including the ability to set new condition values and manage used values. - Refactored the ComponentsPanel to include the new V2 select component with appropriate configuration options. - Improved the V2SelectConfigPanel to streamline option management and enhance user experience.
This commit is contained in:
parent
f8c0fe9499
commit
c65f436009
|
|
@ -1,6 +1,7 @@
|
|||
import React, { useState, useEffect, useCallback } from "react";
|
||||
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 {
|
||||
|
|
@ -78,6 +79,11 @@ export const LayerManagerPanel: React.FC<LayerManagerPanelProps> = ({
|
|||
// 기본 레이어 컴포넌트 로드
|
||||
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) {
|
||||
|
|
@ -86,7 +92,7 @@ export const LayerManagerPanel: React.FC<LayerManagerPanelProps> = ({
|
|||
} catch {
|
||||
setBaseLayerComponents(components);
|
||||
}
|
||||
}, [screenId, components]);
|
||||
}, [screenId, components, activeLayerId]);
|
||||
|
||||
useEffect(() => {
|
||||
loadLayers();
|
||||
|
|
@ -191,6 +197,22 @@ export const LayerManagerPanel: React.FC<LayerManagerPanelProps> = ({
|
|||
["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 (
|
||||
<div className="flex h-full flex-col bg-background">
|
||||
{/* 헤더 */}
|
||||
|
|
@ -335,6 +357,8 @@ export const LayerManagerPanel: React.FC<LayerManagerPanelProps> = ({
|
|||
{/* Zone 소속 레이어 목록 */}
|
||||
{zoneLayers.map((layer) => {
|
||||
const isActive = activeLayerId === layer.layer_id;
|
||||
const triggerOpts = getTriggerOptions(zone);
|
||||
const currentCondValue = layer.condition_config?.condition_value || "";
|
||||
return (
|
||||
<div
|
||||
key={layer.layer_id}
|
||||
|
|
@ -349,10 +373,47 @@ export const LayerManagerPanel: React.FC<LayerManagerPanelProps> = ({
|
|||
<SplitSquareVertical className="h-3 w-3 shrink-0 text-amber-600" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<span className="text-xs font-medium truncate">{layer.layer_name}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1 mt-0.5">
|
||||
{triggerOpts.length > 0 ? (
|
||||
<Select
|
||||
value={currentCondValue || "_none_"}
|
||||
onValueChange={async (val) => {
|
||||
if (!screenId) return;
|
||||
const newVal = val === "_none_" ? "" : val;
|
||||
try {
|
||||
await screenApi.updateLayerCondition(
|
||||
screenId,
|
||||
layer.layer_id,
|
||||
{ ...layer.condition_config, condition_value: newVal },
|
||||
layer.layer_name,
|
||||
);
|
||||
await loadLayers();
|
||||
toast.success("조건값이 변경되었습니다.");
|
||||
} catch {
|
||||
toast.error("조건값 변경에 실패했습니다.");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="h-5 text-[10px] w-auto min-w-[80px] max-w-[140px] px-1.5"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<SelectValue placeholder="조건값 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="_none_" className="text-xs text-muted-foreground">미설정</SelectItem>
|
||||
{triggerOpts.map(opt => (
|
||||
<SelectItem key={opt.value} value={opt.value} className="text-xs">
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
조건값: {layer.condition_config?.condition_value || "미설정"}
|
||||
조건값: {currentCondValue || "미설정"}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
| {layer.component_count}개
|
||||
</span>
|
||||
|
|
@ -373,14 +434,41 @@ export const LayerManagerPanel: React.FC<LayerManagerPanelProps> = ({
|
|||
{/* 레이어 추가 */}
|
||||
{addingToZoneId === zone.zone_id ? (
|
||||
<div className="flex items-center gap-1 rounded border bg-background p-1.5">
|
||||
{(() => {
|
||||
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 (
|
||||
<Select value={newConditionValue} onValueChange={setNewConditionValue}>
|
||||
<SelectTrigger className="h-6 text-[11px] flex-1">
|
||||
<SelectValue placeholder="조건값 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableOpts.map(opt => (
|
||||
<SelectItem key={opt.value} value={opt.value} className="text-xs">
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Input
|
||||
value={newConditionValue}
|
||||
onChange={(e) => setNewConditionValue(e.target.value)}
|
||||
placeholder="조건값 입력 (예: 옵션1)"
|
||||
placeholder="조건값 입력"
|
||||
className="h-6 text-[11px] flex-1"
|
||||
autoFocus
|
||||
onKeyDown={(e) => { if (e.key === "Enter") handleAddLayerToZone(zone.zone_id); }}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
<Button
|
||||
variant="default" size="sm"
|
||||
className="h-6 px-2 text-[10px]"
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -49,7 +49,6 @@ export function ComponentsPanel({
|
|||
() =>
|
||||
[
|
||||
// v2-input: 테이블 컬럼 드래그 시 자동 생성되므로 숨김 처리
|
||||
// v2-select: 테이블 컬럼 드래그 시 자동 생성되므로 숨김 처리
|
||||
// v2-date: 테이블 컬럼 드래그 시 자동 생성되므로 숨김 처리
|
||||
// v2-layout: 중첩 드래그앤드롭 기능 미구현으로 숨김 처리
|
||||
// v2-group: 중첩 드래그앤드롭 기능 미구현으로 숨김 처리
|
||||
|
|
@ -57,6 +56,23 @@ export function ComponentsPanel({
|
|||
// v2-media 제거 - 테이블 컬럼의 image/file 입력 타입으로 사용
|
||||
// v2-biz 제거 - 개별 컴포넌트(flow-widget, rack-structure, numbering-rule)로 직접 표시
|
||||
// v2-hierarchy 제거 - 현재 미사용
|
||||
{
|
||||
id: "v2-select",
|
||||
name: "V2 선택",
|
||||
description: "드롭다운, 콤보박스, 라디오, 체크박스 등 다양한 선택 모드 지원",
|
||||
category: "input" as ComponentCategory,
|
||||
tags: ["select", "dropdown", "combobox", "v2"],
|
||||
defaultSize: { width: 300, height: 40 },
|
||||
defaultConfig: {
|
||||
mode: "dropdown",
|
||||
source: "static",
|
||||
multiple: false,
|
||||
searchable: false,
|
||||
placeholder: "선택하세요",
|
||||
options: [],
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "v2-repeater",
|
||||
name: "리피터 그리드",
|
||||
|
|
@ -65,7 +81,7 @@ export function ComponentsPanel({
|
|||
tags: ["repeater", "table", "modal", "button", "v2", "v2"],
|
||||
defaultSize: { width: 600, height: 300 },
|
||||
},
|
||||
] as ComponentDefinition[],
|
||||
] as unknown as ComponentDefinition[],
|
||||
[],
|
||||
);
|
||||
|
||||
|
|
@ -126,6 +142,7 @@ export function ComponentsPanel({
|
|||
"section-card", // → v2-section-card
|
||||
"location-swap-selector", // → v2-location-swap-selector
|
||||
"rack-structure", // → v2-rack-structure
|
||||
"v2-select", // → v2-select (아래 v2Components에서 별도 처리)
|
||||
"v2-repeater", // → v2-repeater (아래 v2Components에서 별도 처리)
|
||||
"repeat-container", // → v2-repeat-container
|
||||
"repeat-screen-modal", // → v2-repeat-screen-modal
|
||||
|
|
|
|||
|
|
@ -71,7 +71,9 @@ const DropdownSelect = forwardRef<HTMLButtonElement, {
|
|||
<SelectValue placeholder={placeholder} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{options.map((option) => (
|
||||
{options
|
||||
.filter((option) => option.value !== "")
|
||||
.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
|
|
|
|||
|
|
@ -87,9 +87,9 @@ export const V2SelectConfigPanel: React.FC<V2SelectConfigPanelProps> = ({ config
|
|||
updateConfig("options", newOptions);
|
||||
};
|
||||
|
||||
const updateOption = (index: number, field: "value" | "label", value: string) => {
|
||||
const updateOptionValue = (index: number, value: string) => {
|
||||
const newOptions = [...options];
|
||||
newOptions[index] = { ...newOptions[index], [field]: value };
|
||||
newOptions[index] = { ...newOptions[index], value, label: value };
|
||||
updateConfig("options", newOptions);
|
||||
};
|
||||
|
||||
|
|
@ -139,7 +139,7 @@ export const V2SelectConfigPanel: React.FC<V2SelectConfigPanelProps> = ({ config
|
|||
</div>
|
||||
|
||||
{/* 정적 옵션 관리 */}
|
||||
{config.source === "static" && (
|
||||
{(config.source || "static") === "static" && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs font-medium">옵션 목록</Label>
|
||||
|
|
@ -148,19 +148,13 @@ export const V2SelectConfigPanel: React.FC<V2SelectConfigPanelProps> = ({ config
|
|||
추가
|
||||
</Button>
|
||||
</div>
|
||||
<div className="max-h-40 space-y-2 overflow-y-auto">
|
||||
<div className="max-h-40 space-y-1.5 overflow-y-auto">
|
||||
{options.map((option: any, index: number) => (
|
||||
<div key={index} className="flex items-center gap-2">
|
||||
<div key={index} className="flex items-center gap-1.5">
|
||||
<Input
|
||||
value={option.value || ""}
|
||||
onChange={(e) => updateOption(index, "value", e.target.value)}
|
||||
placeholder="값"
|
||||
className="h-7 flex-1 text-xs"
|
||||
/>
|
||||
<Input
|
||||
value={option.label || ""}
|
||||
onChange={(e) => updateOption(index, "label", e.target.value)}
|
||||
placeholder="표시 텍스트"
|
||||
onChange={(e) => updateOptionValue(index, e.target.value)}
|
||||
placeholder={`옵션 ${index + 1}`}
|
||||
className="h-7 flex-1 text-xs"
|
||||
/>
|
||||
<Button
|
||||
|
|
@ -168,7 +162,7 @@ export const V2SelectConfigPanel: React.FC<V2SelectConfigPanelProps> = ({ config
|
|||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => removeOption(index)}
|
||||
className="text-destructive h-7 w-7 p-0"
|
||||
className="text-destructive h-7 w-7 shrink-0 p-0"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
|
|
|
|||
Loading…
Reference in New Issue