버튼 문제 해결
This commit is contained in:
parent
fa30763ae2
commit
10d112bd69
|
|
@ -420,7 +420,9 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const targetComponent = layout.components.find((comp) => comp.id === componentId);
|
// 🔥 함수형 업데이트로 변경하여 최신 layout 사용
|
||||||
|
setLayout((prevLayout) => {
|
||||||
|
const targetComponent = prevLayout.components.find((comp) => comp.id === componentId);
|
||||||
const isLayoutComponent = targetComponent?.type === "layout";
|
const isLayoutComponent = targetComponent?.type === "layout";
|
||||||
|
|
||||||
// 레이아웃 컴포넌트의 위치가 변경되는 경우 존에 속한 컴포넌트들도 함께 이동
|
// 레이아웃 컴포넌트의 위치가 변경되는 경우 존에 속한 컴포넌트들도 함께 이동
|
||||||
|
|
@ -450,7 +452,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
}
|
}
|
||||||
|
|
||||||
const pathParts = path.split(".");
|
const pathParts = path.split(".");
|
||||||
const updatedComponents = layout.components.map((comp) => {
|
const updatedComponents = prevLayout.components.map((comp) => {
|
||||||
if (comp.id !== componentId) {
|
if (comp.id !== componentId) {
|
||||||
// 레이아웃 이동 시 존에 속한 컴포넌트들도 함께 이동
|
// 레이아웃 이동 시 존에 속한 컴포넌트들도 함께 이동
|
||||||
if (isLayoutComponent && (positionDelta.x !== 0 || positionDelta.y !== 0)) {
|
if (isLayoutComponent && (positionDelta.x !== 0 || positionDelta.y !== 0)) {
|
||||||
|
|
@ -480,22 +482,35 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
// 중첩 경로를 고려한 안전한 복사
|
// 중첩 경로를 고려한 안전한 복사
|
||||||
const newComp = { ...comp };
|
const newComp = { ...comp };
|
||||||
|
|
||||||
|
console.log("🔍 업데이트 전 상태:", {
|
||||||
|
path,
|
||||||
|
value,
|
||||||
|
"기존 componentConfig": newComp.componentConfig,
|
||||||
|
"기존 action": (newComp as any).componentConfig?.action,
|
||||||
|
});
|
||||||
|
|
||||||
// 경로를 따라 내려가면서 각 레벨을 새 객체로 복사
|
// 경로를 따라 내려가면서 각 레벨을 새 객체로 복사
|
||||||
let current: any = newComp;
|
let current: any = newComp;
|
||||||
for (let i = 0; i < pathParts.length - 1; i++) {
|
for (let i = 0; i < pathParts.length - 1; i++) {
|
||||||
const key = pathParts[i];
|
const key = pathParts[i];
|
||||||
|
console.log(`🔍 경로 탐색 [${i}]: key="${key}", current[key]=`, current[key]);
|
||||||
|
|
||||||
// 다음 레벨이 없거나 객체가 아니면 새 객체 생성
|
// 다음 레벨이 없거나 객체가 아니면 새 객체 생성
|
||||||
if (!current[key] || typeof current[key] !== "object" || Array.isArray(current[key])) {
|
if (!current[key] || typeof current[key] !== "object" || Array.isArray(current[key])) {
|
||||||
|
console.log(`🆕 새 객체 생성: ${key}`);
|
||||||
current[key] = {};
|
current[key] = {};
|
||||||
} else {
|
} else {
|
||||||
// 기존 객체를 복사하여 불변성 유지
|
// 기존 객체를 복사하여 불변성 유지
|
||||||
|
console.log(`📋 기존 객체 복사: ${key}`, { ...current[key] });
|
||||||
current[key] = { ...current[key] };
|
current[key] = { ...current[key] };
|
||||||
}
|
}
|
||||||
current = current[key];
|
current = current[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 최종 값 설정
|
// 최종 값 설정
|
||||||
current[pathParts[pathParts.length - 1]] = value;
|
const finalKey = pathParts[pathParts.length - 1];
|
||||||
|
console.log(`✍️ 최종 값 설정: ${finalKey} = ${value}`);
|
||||||
|
current[finalKey] = value;
|
||||||
|
|
||||||
console.log("✅ 컴포넌트 업데이트 완료:", {
|
console.log("✅ 컴포넌트 업데이트 완료:", {
|
||||||
componentId,
|
componentId,
|
||||||
|
|
@ -551,25 +566,25 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
// 크기 변경 시 격자 스냅 적용 (그룹 컴포넌트 제외)
|
// 크기 변경 시 격자 스냅 적용 (그룹 컴포넌트 제외)
|
||||||
if (
|
if (
|
||||||
(path === "size.width" || path === "size.height") &&
|
(path === "size.width" || path === "size.height") &&
|
||||||
layout.gridSettings?.snapToGrid &&
|
prevLayout.gridSettings?.snapToGrid &&
|
||||||
gridInfo &&
|
gridInfo &&
|
||||||
newComp.type !== "group"
|
newComp.type !== "group"
|
||||||
) {
|
) {
|
||||||
// 현재 해상도에 맞는 격자 정보로 스냅 적용
|
// 현재 해상도에 맞는 격자 정보로 스냅 적용
|
||||||
const currentGridInfo = calculateGridInfo(screenResolution.width, screenResolution.height, {
|
const currentGridInfo = calculateGridInfo(screenResolution.width, screenResolution.height, {
|
||||||
columns: layout.gridSettings.columns,
|
columns: prevLayout.gridSettings.columns,
|
||||||
gap: layout.gridSettings.gap,
|
gap: prevLayout.gridSettings.gap,
|
||||||
padding: layout.gridSettings.padding,
|
padding: prevLayout.gridSettings.padding,
|
||||||
snapToGrid: layout.gridSettings.snapToGrid || false,
|
snapToGrid: prevLayout.gridSettings.snapToGrid || false,
|
||||||
});
|
});
|
||||||
const snappedSize = snapSizeToGrid(newComp.size, currentGridInfo, layout.gridSettings as GridUtilSettings);
|
const snappedSize = snapSizeToGrid(newComp.size, currentGridInfo, prevLayout.gridSettings as GridUtilSettings);
|
||||||
newComp.size = snappedSize;
|
newComp.size = snappedSize;
|
||||||
|
|
||||||
// 크기 변경 시 gridColumns도 자동 조정
|
// 크기 변경 시 gridColumns도 자동 조정
|
||||||
const adjustedColumns = adjustGridColumnsFromSize(
|
const adjustedColumns = adjustGridColumnsFromSize(
|
||||||
newComp,
|
newComp,
|
||||||
currentGridInfo,
|
currentGridInfo,
|
||||||
layout.gridSettings as GridUtilSettings,
|
prevLayout.gridSettings as GridUtilSettings,
|
||||||
);
|
);
|
||||||
if (newComp.gridColumns !== adjustedColumns) {
|
if (newComp.gridColumns !== adjustedColumns) {
|
||||||
newComp.gridColumns = adjustedColumns;
|
newComp.gridColumns = adjustedColumns;
|
||||||
|
|
@ -582,19 +597,19 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
}
|
}
|
||||||
|
|
||||||
// gridColumns 변경 시 크기를 격자에 맞게 자동 조정
|
// gridColumns 변경 시 크기를 격자에 맞게 자동 조정
|
||||||
if (path === "gridColumns" && layout.gridSettings?.snapToGrid && newComp.type !== "group") {
|
if (path === "gridColumns" && prevLayout.gridSettings?.snapToGrid && newComp.type !== "group") {
|
||||||
const currentGridInfo = calculateGridInfo(screenResolution.width, screenResolution.height, {
|
const currentGridInfo = calculateGridInfo(screenResolution.width, screenResolution.height, {
|
||||||
columns: layout.gridSettings.columns,
|
columns: prevLayout.gridSettings.columns,
|
||||||
gap: layout.gridSettings.gap,
|
gap: prevLayout.gridSettings.gap,
|
||||||
padding: layout.gridSettings.padding,
|
padding: prevLayout.gridSettings.padding,
|
||||||
snapToGrid: layout.gridSettings.snapToGrid || false,
|
snapToGrid: prevLayout.gridSettings.snapToGrid || false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// gridColumns에 맞는 정확한 너비 계산
|
// gridColumns에 맞는 정확한 너비 계산
|
||||||
const newWidth = calculateWidthFromColumns(
|
const newWidth = calculateWidthFromColumns(
|
||||||
newComp.gridColumns,
|
newComp.gridColumns,
|
||||||
currentGridInfo,
|
currentGridInfo,
|
||||||
layout.gridSettings as GridUtilSettings,
|
prevLayout.gridSettings as GridUtilSettings,
|
||||||
);
|
);
|
||||||
newComp.size = {
|
newComp.size = {
|
||||||
...newComp.size,
|
...newComp.size,
|
||||||
|
|
@ -702,30 +717,46 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
return newComp;
|
return newComp;
|
||||||
});
|
});
|
||||||
|
|
||||||
const newLayout = { ...layout, components: updatedComponents };
|
// 🔥 새로운 layout 생성
|
||||||
setLayout(newLayout);
|
const newLayout = { ...prevLayout, components: updatedComponents };
|
||||||
|
|
||||||
|
console.log("🔄 setLayout 실행:", {
|
||||||
|
componentId,
|
||||||
|
path,
|
||||||
|
value,
|
||||||
|
업데이트된컴포넌트: updatedComponents.find((c) => c.id === componentId),
|
||||||
|
});
|
||||||
|
|
||||||
saveToHistory(newLayout);
|
saveToHistory(newLayout);
|
||||||
|
|
||||||
// selectedComponent가 업데이트된 컴포넌트와 같다면 selectedComponent도 업데이트
|
// selectedComponent가 업데이트된 컴포넌트와 같다면 selectedComponent도 업데이트
|
||||||
if (selectedComponent && selectedComponent.id === componentId) {
|
setSelectedComponent((prevSelected) => {
|
||||||
|
if (prevSelected && prevSelected.id === componentId) {
|
||||||
const updatedSelectedComponent = updatedComponents.find((c) => c.id === componentId);
|
const updatedSelectedComponent = updatedComponents.find((c) => c.id === componentId);
|
||||||
if (updatedSelectedComponent) {
|
if (updatedSelectedComponent) {
|
||||||
|
// 🔧 완전히 새로운 객체를 만들어서 React가 변경을 감지하도록 함
|
||||||
|
const newSelectedComponent = JSON.parse(JSON.stringify(updatedSelectedComponent));
|
||||||
|
|
||||||
console.log("🔄 selectedComponent 동기화:", {
|
console.log("🔄 selectedComponent 동기화:", {
|
||||||
componentId,
|
componentId,
|
||||||
path,
|
path,
|
||||||
|
oldAction: (prevSelected as any).componentConfig?.action,
|
||||||
|
newAction: (newSelectedComponent as any).componentConfig?.action,
|
||||||
oldColumnsCount:
|
oldColumnsCount:
|
||||||
selectedComponent.type === "datatable" ? (selectedComponent as any).columns?.length : "N/A",
|
prevSelected.type === "datatable" ? (prevSelected as any).columns?.length : "N/A",
|
||||||
newColumnsCount:
|
newColumnsCount:
|
||||||
updatedSelectedComponent.type === "datatable" ? (updatedSelectedComponent as any).columns?.length : "N/A",
|
newSelectedComponent.type === "datatable" ? (newSelectedComponent as any).columns?.length : "N/A",
|
||||||
oldFiltersCount:
|
oldFiltersCount:
|
||||||
selectedComponent.type === "datatable" ? (selectedComponent as any).filters?.length : "N/A",
|
prevSelected.type === "datatable" ? (prevSelected as any).filters?.length : "N/A",
|
||||||
newFiltersCount:
|
newFiltersCount:
|
||||||
updatedSelectedComponent.type === "datatable" ? (updatedSelectedComponent as any).filters?.length : "N/A",
|
newSelectedComponent.type === "datatable" ? (newSelectedComponent as any).filters?.length : "N/A",
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
setSelectedComponent(updatedSelectedComponent);
|
return newSelectedComponent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return prevSelected;
|
||||||
|
});
|
||||||
|
|
||||||
// webTypeConfig 업데이트 후 레이아웃 상태 확인
|
// webTypeConfig 업데이트 후 레이아웃 상태 확인
|
||||||
if (path === "webTypeConfig") {
|
if (path === "webTypeConfig") {
|
||||||
|
|
@ -743,8 +774,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return newLayout;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[layout, gridInfo, saveToHistory],
|
[gridInfo, saveToHistory], // 🔧 layout, selectedComponent 제거!
|
||||||
);
|
);
|
||||||
|
|
||||||
// 컴포넌트 시스템 초기화
|
// 컴포넌트 시스템 초기화
|
||||||
|
|
@ -1294,11 +1328,22 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
components: updatedComponents,
|
components: updatedComponents,
|
||||||
screenResolution: screenResolution,
|
screenResolution: screenResolution,
|
||||||
};
|
};
|
||||||
|
// 🔍 버튼 컴포넌트들의 action.type 확인
|
||||||
|
const buttonComponents = layoutWithResolution.components.filter(
|
||||||
|
(c: any) => c.type === "button" || c.type === "button-primary" || c.type === "button-secondary"
|
||||||
|
);
|
||||||
console.log("💾 저장 시작:", {
|
console.log("💾 저장 시작:", {
|
||||||
screenId: selectedScreen.screenId,
|
screenId: selectedScreen.screenId,
|
||||||
componentsCount: layoutWithResolution.components.length,
|
componentsCount: layoutWithResolution.components.length,
|
||||||
gridSettings: layoutWithResolution.gridSettings,
|
gridSettings: layoutWithResolution.gridSettings,
|
||||||
screenResolution: layoutWithResolution.screenResolution,
|
screenResolution: layoutWithResolution.screenResolution,
|
||||||
|
buttonComponents: buttonComponents.map((c: any) => ({
|
||||||
|
id: c.id,
|
||||||
|
type: c.type,
|
||||||
|
text: c.componentConfig?.text,
|
||||||
|
actionType: c.componentConfig?.action?.type,
|
||||||
|
fullAction: c.componentConfig?.action,
|
||||||
|
})),
|
||||||
});
|
});
|
||||||
|
|
||||||
await screenApi.saveLayout(selectedScreen.screenId, layoutWithResolution);
|
await screenApi.saveLayout(selectedScreen.screenId, layoutWithResolution);
|
||||||
|
|
@ -2127,7 +2172,8 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
|
|
||||||
// 새 컴포넌트 선택
|
// 새 컴포넌트 선택
|
||||||
setSelectedComponent(newComponent);
|
setSelectedComponent(newComponent);
|
||||||
openPanel("properties");
|
// 🔧 테이블 패널 유지를 위해 자동 속성 패널 열기 비활성화
|
||||||
|
// openPanel("properties");
|
||||||
|
|
||||||
toast.success(`${component.name} 컴포넌트가 추가되었습니다.`);
|
toast.success(`${component.name} 컴포넌트가 추가되었습니다.`);
|
||||||
},
|
},
|
||||||
|
|
@ -2610,8 +2656,8 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
saveToHistory(newLayout);
|
saveToHistory(newLayout);
|
||||||
setSelectedComponent(newComponent);
|
setSelectedComponent(newComponent);
|
||||||
|
|
||||||
// 속성 패널 자동 열기
|
// 🔧 테이블 패널 유지를 위해 자동 속성 패널 열기 비활성화
|
||||||
openPanel("properties");
|
// openPanel("properties");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// console.error("드롭 처리 실패:", error);
|
// console.error("드롭 처리 실패:", error);
|
||||||
}
|
}
|
||||||
|
|
@ -2674,47 +2720,66 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🔧 layout.components에서 최신 버전의 컴포넌트 찾기
|
||||||
|
const latestComponent = layout.components.find((c) => c.id === component.id);
|
||||||
|
if (!latestComponent) {
|
||||||
|
console.warn("⚠️ 컴포넌트를 찾을 수 없습니다:", component.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("🔍 컴포넌트 클릭 시 최신 버전 확인:", {
|
||||||
|
componentId: component.id,
|
||||||
|
파라미터로받은버전: {
|
||||||
|
actionType: (component as any).componentConfig?.action?.type,
|
||||||
|
fullAction: (component as any).componentConfig?.action,
|
||||||
|
},
|
||||||
|
layout에서찾은최신버전: {
|
||||||
|
actionType: (latestComponent as any).componentConfig?.action?.type,
|
||||||
|
fullAction: (latestComponent as any).componentConfig?.action,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const isShiftPressed = event?.shiftKey || false;
|
const isShiftPressed = event?.shiftKey || false;
|
||||||
const isCtrlPressed = event?.ctrlKey || event?.metaKey || false;
|
const isCtrlPressed = event?.ctrlKey || event?.metaKey || false;
|
||||||
const isGroupContainer = component.type === "group";
|
const isGroupContainer = latestComponent.type === "group";
|
||||||
|
|
||||||
if (isShiftPressed || isCtrlPressed || groupState.isGrouping) {
|
if (isShiftPressed || isCtrlPressed || groupState.isGrouping) {
|
||||||
// 다중 선택 모드
|
// 다중 선택 모드
|
||||||
if (isGroupContainer) {
|
if (isGroupContainer) {
|
||||||
// 그룹 컨테이너는 단일 선택으로 처리
|
// 그룹 컨테이너는 단일 선택으로 처리
|
||||||
handleComponentSelect(component);
|
handleComponentSelect(latestComponent); // 🔧 최신 버전 사용
|
||||||
setGroupState((prev) => ({
|
setGroupState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
selectedComponents: [component.id],
|
selectedComponents: [latestComponent.id],
|
||||||
isGrouping: false,
|
isGrouping: false,
|
||||||
}));
|
}));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSelected = groupState.selectedComponents.includes(component.id);
|
const isSelected = groupState.selectedComponents.includes(latestComponent.id);
|
||||||
setGroupState((prev) => ({
|
setGroupState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
selectedComponents: isSelected
|
selectedComponents: isSelected
|
||||||
? prev.selectedComponents.filter((id) => id !== component.id)
|
? prev.selectedComponents.filter((id) => id !== latestComponent.id)
|
||||||
: [...prev.selectedComponents, component.id],
|
: [...prev.selectedComponents, latestComponent.id],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 마지막 선택된 컴포넌트를 selectedComponent로 설정
|
// 마지막 선택된 컴포넌트를 selectedComponent로 설정
|
||||||
if (!isSelected) {
|
if (!isSelected) {
|
||||||
// console.log("🎯 컴포넌트 선택 (다중 모드):", component.id);
|
// console.log("🎯 컴포넌트 선택 (다중 모드):", latestComponent.id);
|
||||||
handleComponentSelect(component);
|
handleComponentSelect(latestComponent); // 🔧 최신 버전 사용
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 단일 선택 모드
|
// 단일 선택 모드
|
||||||
// console.log("🎯 컴포넌트 선택 (단일 모드):", component.id);
|
// console.log("🎯 컴포넌트 선택 (단일 모드):", latestComponent.id);
|
||||||
handleComponentSelect(component);
|
handleComponentSelect(latestComponent); // 🔧 최신 버전 사용
|
||||||
setGroupState((prev) => ({
|
setGroupState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
selectedComponents: [component.id],
|
selectedComponents: [latestComponent.id],
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[handleComponentSelect, groupState.isGrouping, groupState.selectedComponents, dragState.justFinishedDrag],
|
[handleComponentSelect, groupState.isGrouping, groupState.selectedComponents, dragState.justFinishedDrag, layout.components],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 컴포넌트 드래그 시작
|
// 컴포넌트 드래그 시작
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,625 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Check, ChevronsUpDown, Search } from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { ComponentData } from "@/types/screen";
|
||||||
|
import { apiClient } from "@/lib/api/client";
|
||||||
|
import { ButtonDataflowConfigPanel } from "./ButtonDataflowConfigPanel";
|
||||||
|
import { ImprovedButtonControlConfigPanel } from "./ImprovedButtonControlConfigPanel";
|
||||||
|
|
||||||
|
interface ButtonConfigPanelProps {
|
||||||
|
component: ComponentData;
|
||||||
|
onUpdateProperty: (path: string, value: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ScreenOption {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component, onUpdateProperty }) => {
|
||||||
|
// 🔧 항상 최신 component에서 직접 참조
|
||||||
|
const config = component.componentConfig || {};
|
||||||
|
const currentAction = component.componentConfig?.action || {}; // 🔧 최신 action 참조
|
||||||
|
|
||||||
|
// 로컬 상태 관리 (실시간 입력 반영)
|
||||||
|
const [localInputs, setLocalInputs] = useState({
|
||||||
|
text: config.text !== undefined ? config.text : "버튼", // 🔧 빈 문자열 허용
|
||||||
|
modalTitle: config.action?.modalTitle || "",
|
||||||
|
editModalTitle: config.action?.editModalTitle || "",
|
||||||
|
editModalDescription: config.action?.editModalDescription || "",
|
||||||
|
targetUrl: config.action?.targetUrl || "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const [localSelects, setLocalSelects] = useState({
|
||||||
|
variant: config.variant || "default",
|
||||||
|
size: config.size || "md", // 🔧 기본값을 "md"로 변경
|
||||||
|
actionType: config.action?.type, // 🔧 기본값 완전 제거 (undefined)
|
||||||
|
modalSize: config.action?.modalSize || "md",
|
||||||
|
editMode: config.action?.editMode || "modal",
|
||||||
|
});
|
||||||
|
|
||||||
|
const [screens, setScreens] = useState<ScreenOption[]>([]);
|
||||||
|
const [screensLoading, setScreensLoading] = useState(false);
|
||||||
|
const [modalScreenOpen, setModalScreenOpen] = useState(false);
|
||||||
|
const [navScreenOpen, setNavScreenOpen] = useState(false);
|
||||||
|
const [modalSearchTerm, setModalSearchTerm] = useState("");
|
||||||
|
const [navSearchTerm, setNavSearchTerm] = useState("");
|
||||||
|
|
||||||
|
// 컴포넌트 변경 시 로컬 상태 동기화
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("🔄 ButtonConfigPanel useEffect 실행:", {
|
||||||
|
componentId: component.id,
|
||||||
|
"config.action?.type": config.action?.type,
|
||||||
|
"localSelects.actionType (before)": localSelects.actionType,
|
||||||
|
fullAction: config.action,
|
||||||
|
"component.componentConfig.action": component.componentConfig?.action,
|
||||||
|
});
|
||||||
|
|
||||||
|
setLocalInputs({
|
||||||
|
text: config.text !== undefined ? config.text : "버튼", // 🔧 빈 문자열 허용
|
||||||
|
modalTitle: config.action?.modalTitle || "",
|
||||||
|
editModalTitle: config.action?.editModalTitle || "",
|
||||||
|
editModalDescription: config.action?.editModalDescription || "",
|
||||||
|
targetUrl: config.action?.targetUrl || "",
|
||||||
|
});
|
||||||
|
|
||||||
|
setLocalSelects((prev) => {
|
||||||
|
const newSelects = {
|
||||||
|
variant: config.variant || "default",
|
||||||
|
size: config.size || "md", // 🔧 기본값을 "md"로 변경
|
||||||
|
actionType: config.action?.type, // 🔧 기본값 완전 제거 (undefined)
|
||||||
|
modalSize: config.action?.modalSize || "md",
|
||||||
|
editMode: config.action?.editMode || "modal",
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("📝 setLocalSelects 호출:", {
|
||||||
|
"prev.actionType": prev.actionType,
|
||||||
|
"new.actionType": newSelects.actionType,
|
||||||
|
"config.action?.type": config.action?.type,
|
||||||
|
});
|
||||||
|
|
||||||
|
return newSelects;
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
component.id, // 🔧 컴포넌트 ID (다른 컴포넌트로 전환 시)
|
||||||
|
component.componentConfig?.action?.type, // 🔧 액션 타입 (액션 변경 시 즉시 반영)
|
||||||
|
component.componentConfig?.text, // 🔧 버튼 텍스트
|
||||||
|
component.componentConfig?.variant, // 🔧 버튼 스타일
|
||||||
|
component.componentConfig?.size, // 🔧 버튼 크기
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 화면 목록 가져오기
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchScreens = async () => {
|
||||||
|
try {
|
||||||
|
setScreensLoading(true);
|
||||||
|
const response = await apiClient.get("/screen-management/screens");
|
||||||
|
|
||||||
|
if (response.data.success && Array.isArray(response.data.data)) {
|
||||||
|
const screenList = response.data.data.map((screen: any) => ({
|
||||||
|
id: screen.screenId,
|
||||||
|
name: screen.screenName,
|
||||||
|
description: screen.description,
|
||||||
|
}));
|
||||||
|
setScreens(screenList);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// console.error("❌ 화면 목록 로딩 실패:", error);
|
||||||
|
} finally {
|
||||||
|
setScreensLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchScreens();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 검색 필터링 함수
|
||||||
|
const filterScreens = (searchTerm: string) => {
|
||||||
|
if (!searchTerm.trim()) return screens;
|
||||||
|
return screens.filter(
|
||||||
|
(screen) =>
|
||||||
|
screen.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
(screen.description && screen.description.toLowerCase().includes(searchTerm.toLowerCase())),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("🔧 config-panels/ButtonConfigPanel 렌더링:", {
|
||||||
|
component,
|
||||||
|
config,
|
||||||
|
action: config.action,
|
||||||
|
actionType: config.action?.type,
|
||||||
|
screensCount: screens.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="button-text">버튼 텍스트</Label>
|
||||||
|
<Input
|
||||||
|
id="button-text"
|
||||||
|
value={localInputs.text}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newValue = e.target.value;
|
||||||
|
setLocalInputs((prev) => ({ ...prev, text: newValue }));
|
||||||
|
onUpdateProperty("componentConfig.text", newValue);
|
||||||
|
}}
|
||||||
|
placeholder="버튼 텍스트를 입력하세요"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="button-variant">버튼 스타일</Label>
|
||||||
|
<Select
|
||||||
|
value={localSelects.variant}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setLocalSelects((prev) => ({ ...prev, variant: value }));
|
||||||
|
onUpdateProperty("componentConfig.variant", value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="버튼 스타일 선택" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="primary">기본 (Primary)</SelectItem>
|
||||||
|
<SelectItem value="secondary">보조 (Secondary)</SelectItem>
|
||||||
|
<SelectItem value="danger">위험 (Danger)</SelectItem>
|
||||||
|
<SelectItem value="success">성공 (Success)</SelectItem>
|
||||||
|
<SelectItem value="outline">외곽선 (Outline)</SelectItem>
|
||||||
|
<SelectItem value="ghost">고스트 (Ghost)</SelectItem>
|
||||||
|
<SelectItem value="link">링크 (Link)</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="button-size">버튼 글씨 크기</Label>
|
||||||
|
<Select
|
||||||
|
value={localSelects.size}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setLocalSelects((prev) => ({ ...prev, size: value }));
|
||||||
|
onUpdateProperty("componentConfig.size", value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="버튼 글씨 크기 선택" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="sm">작음 (Small)</SelectItem>
|
||||||
|
<SelectItem value="md">기본 (Default)</SelectItem>
|
||||||
|
<SelectItem value="lg">큼 (Large)</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="button-action">버튼 액션</Label>
|
||||||
|
<Select
|
||||||
|
value={localSelects.actionType || undefined}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
console.log("🔵 버튼 액션 변경 시작:", {
|
||||||
|
oldValue: localSelects.actionType,
|
||||||
|
newValue: value,
|
||||||
|
componentId: component.id,
|
||||||
|
"현재 component.componentConfig.action": component.componentConfig?.action,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 로컬 상태 업데이트
|
||||||
|
setLocalSelects((prev) => {
|
||||||
|
console.log("📝 setLocalSelects (액션 변경):", {
|
||||||
|
"prev.actionType": prev.actionType,
|
||||||
|
"new.actionType": value,
|
||||||
|
});
|
||||||
|
return { ...prev, actionType: value };
|
||||||
|
});
|
||||||
|
|
||||||
|
// 🔧 개별 속성만 업데이트
|
||||||
|
onUpdateProperty("componentConfig.action.type", value);
|
||||||
|
|
||||||
|
// 액션에 따른 라벨 색상 자동 설정 (별도 호출)
|
||||||
|
if (value === "delete") {
|
||||||
|
onUpdateProperty("style.labelColor", "#ef4444");
|
||||||
|
} else {
|
||||||
|
onUpdateProperty("style.labelColor", "#212121");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("✅ 버튼 액션 변경 완료");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="버튼 액션 선택" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="save">저장</SelectItem>
|
||||||
|
<SelectItem value="cancel">취소</SelectItem>
|
||||||
|
<SelectItem value="delete">삭제</SelectItem>
|
||||||
|
<SelectItem value="edit">수정</SelectItem>
|
||||||
|
<SelectItem value="add">추가</SelectItem>
|
||||||
|
<SelectItem value="search">검색</SelectItem>
|
||||||
|
<SelectItem value="reset">초기화</SelectItem>
|
||||||
|
<SelectItem value="submit">제출</SelectItem>
|
||||||
|
<SelectItem value="close">닫기</SelectItem>
|
||||||
|
<SelectItem value="modal">모달 열기</SelectItem>
|
||||||
|
<SelectItem value="navigate">페이지 이동</SelectItem>
|
||||||
|
<SelectItem value="control">제어 (조건 체크만)</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 모달 열기 액션 설정 */}
|
||||||
|
{localSelects.actionType === "modal" && (
|
||||||
|
<div className="mt-4 space-y-4 rounded-lg border bg-gray-50 p-4">
|
||||||
|
<h4 className="text-sm font-medium text-gray-700">모달 설정</h4>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="modal-title">모달 제목</Label>
|
||||||
|
<Input
|
||||||
|
id="modal-title"
|
||||||
|
placeholder="모달 제목을 입력하세요"
|
||||||
|
value={localInputs.modalTitle}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newValue = e.target.value;
|
||||||
|
setLocalInputs((prev) => ({ ...prev, modalTitle: newValue }));
|
||||||
|
onUpdateProperty("componentConfig.action.modalTitle", newValue);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="modal-size">모달 크기</Label>
|
||||||
|
<Select
|
||||||
|
value={localSelects.modalSize}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setLocalSelects((prev) => ({ ...prev, modalSize: value }));
|
||||||
|
onUpdateProperty("componentConfig.action.modalSize", value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="모달 크기 선택" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="sm">작음 (Small)</SelectItem>
|
||||||
|
<SelectItem value="md">보통 (Medium)</SelectItem>
|
||||||
|
<SelectItem value="lg">큼 (Large)</SelectItem>
|
||||||
|
<SelectItem value="xl">매우 큼 (Extra Large)</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="target-screen-modal">대상 화면 선택</Label>
|
||||||
|
<Popover open={modalScreenOpen} onOpenChange={setModalScreenOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded={modalScreenOpen}
|
||||||
|
className="h-10 w-full justify-between"
|
||||||
|
disabled={screensLoading}
|
||||||
|
>
|
||||||
|
{config.action?.targetScreenId
|
||||||
|
? screens.find((screen) => screen.id === config.action?.targetScreenId)?.name ||
|
||||||
|
"화면을 선택하세요..."
|
||||||
|
: "화면을 선택하세요..."}
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="p-0" align="start" style={{ width: "var(--radix-popover-trigger-width)" }}>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex items-center border-b px-3 py-2">
|
||||||
|
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
<Input
|
||||||
|
placeholder="화면 검색..."
|
||||||
|
value={modalSearchTerm}
|
||||||
|
onChange={(e) => setModalSearchTerm(e.target.value)}
|
||||||
|
className="border-0 p-0 focus-visible:ring-0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="max-h-[200px] overflow-auto">
|
||||||
|
{(() => {
|
||||||
|
const filteredScreens = filterScreens(modalSearchTerm);
|
||||||
|
if (screensLoading) {
|
||||||
|
return <div className="p-3 text-sm text-gray-500">화면 목록을 불러오는 중...</div>;
|
||||||
|
}
|
||||||
|
if (filteredScreens.length === 0) {
|
||||||
|
return <div className="p-3 text-sm text-gray-500">검색 결과가 없습니다.</div>;
|
||||||
|
}
|
||||||
|
return filteredScreens.map((screen, index) => (
|
||||||
|
<div
|
||||||
|
key={`modal-screen-${screen.id}-${index}`}
|
||||||
|
className="flex cursor-pointer items-center px-3 py-2 hover:bg-gray-100"
|
||||||
|
onClick={() => {
|
||||||
|
onUpdateProperty("componentConfig.action.targetScreenId", screen.id);
|
||||||
|
setModalScreenOpen(false);
|
||||||
|
setModalSearchTerm("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-4 w-4",
|
||||||
|
config.action?.targetScreenId === screen.id ? "opacity-100" : "opacity-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium">{screen.name}</span>
|
||||||
|
{screen.description && <span className="text-xs text-gray-500">{screen.description}</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 수정 액션 설정 */}
|
||||||
|
{localSelects.actionType === "edit" && (
|
||||||
|
<div className="mt-4 space-y-4 rounded-lg border bg-green-50 p-4">
|
||||||
|
<h4 className="text-sm font-medium text-gray-700">수정 설정</h4>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="edit-screen">수정 폼 화면 선택</Label>
|
||||||
|
<Popover open={modalScreenOpen} onOpenChange={setModalScreenOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded={modalScreenOpen}
|
||||||
|
className="h-10 w-full justify-between"
|
||||||
|
disabled={screensLoading}
|
||||||
|
>
|
||||||
|
{config.action?.targetScreenId
|
||||||
|
? screens.find((screen) => screen.id === config.action?.targetScreenId)?.name ||
|
||||||
|
"수정 폼 화면을 선택하세요..."
|
||||||
|
: "수정 폼 화면을 선택하세요..."}
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="p-0" align="start" style={{ width: "var(--radix-popover-trigger-width)" }}>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex items-center border-b px-3 py-2">
|
||||||
|
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
<Input
|
||||||
|
placeholder="화면 검색..."
|
||||||
|
value={modalSearchTerm}
|
||||||
|
onChange={(e) => setModalSearchTerm(e.target.value)}
|
||||||
|
className="border-0 p-0 focus-visible:ring-0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="max-h-[200px] overflow-auto">
|
||||||
|
{(() => {
|
||||||
|
const filteredScreens = filterScreens(modalSearchTerm);
|
||||||
|
if (screensLoading) {
|
||||||
|
return <div className="p-3 text-sm text-gray-500">화면 목록을 불러오는 중...</div>;
|
||||||
|
}
|
||||||
|
if (filteredScreens.length === 0) {
|
||||||
|
return <div className="p-3 text-sm text-gray-500">검색 결과가 없습니다.</div>;
|
||||||
|
}
|
||||||
|
return filteredScreens.map((screen, index) => (
|
||||||
|
<div
|
||||||
|
key={`edit-screen-${screen.id}-${index}`}
|
||||||
|
className="flex cursor-pointer items-center px-3 py-2 hover:bg-gray-100"
|
||||||
|
onClick={() => {
|
||||||
|
onUpdateProperty("componentConfig.action.targetScreenId", screen.id);
|
||||||
|
setModalScreenOpen(false);
|
||||||
|
setModalSearchTerm("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-4 w-4",
|
||||||
|
config.action?.targetScreenId === screen.id ? "opacity-100" : "opacity-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium">{screen.name}</span>
|
||||||
|
{screen.description && <span className="text-xs text-gray-500">{screen.description}</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<p className="mt-1 text-xs text-gray-500">
|
||||||
|
선택된 데이터가 이 폼 화면에 자동으로 로드되어 수정할 수 있습니다
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="edit-mode">수정 모드</Label>
|
||||||
|
<Select
|
||||||
|
value={localSelects.editMode}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setLocalSelects((prev) => ({ ...prev, editMode: value }));
|
||||||
|
onUpdateProperty("componentConfig.action.editMode", value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="수정 모드 선택" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="modal">모달로 열기</SelectItem>
|
||||||
|
<SelectItem value="navigate">새 페이지로 이동</SelectItem>
|
||||||
|
<SelectItem value="inline">현재 화면에서 수정</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{localSelects.editMode === "modal" && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="edit-modal-title">모달 제목</Label>
|
||||||
|
<Input
|
||||||
|
id="edit-modal-title"
|
||||||
|
placeholder="모달 제목을 입력하세요 (예: 데이터 수정)"
|
||||||
|
value={localInputs.editModalTitle}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newValue = e.target.value;
|
||||||
|
setLocalInputs((prev) => ({ ...prev, editModalTitle: newValue }));
|
||||||
|
onUpdateProperty("componentConfig.action.editModalTitle", newValue);
|
||||||
|
onUpdateProperty("webTypeConfig.editModalTitle", newValue);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<p className="mt-1 text-xs text-gray-500">비워두면 기본 제목이 표시됩니다</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="edit-modal-description">모달 설명</Label>
|
||||||
|
<Input
|
||||||
|
id="edit-modal-description"
|
||||||
|
placeholder="모달 설명을 입력하세요 (예: 선택한 데이터를 수정합니다)"
|
||||||
|
value={localInputs.editModalDescription}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newValue = e.target.value;
|
||||||
|
setLocalInputs((prev) => ({ ...prev, editModalDescription: newValue }));
|
||||||
|
onUpdateProperty("componentConfig.action.editModalDescription", newValue);
|
||||||
|
onUpdateProperty("webTypeConfig.editModalDescription", newValue);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<p className="mt-1 text-xs text-gray-500">비워두면 설명이 표시되지 않습니다</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="edit-modal-size">모달 크기</Label>
|
||||||
|
<Select
|
||||||
|
value={localSelects.modalSize}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setLocalSelects((prev) => ({ ...prev, modalSize: value }));
|
||||||
|
onUpdateProperty("componentConfig.action.modalSize", value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="모달 크기 선택" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="sm">작음 (Small)</SelectItem>
|
||||||
|
<SelectItem value="md">보통 (Medium)</SelectItem>
|
||||||
|
<SelectItem value="lg">큼 (Large)</SelectItem>
|
||||||
|
<SelectItem value="xl">매우 큼 (Extra Large)</SelectItem>
|
||||||
|
<SelectItem value="full">전체 화면 (Full)</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 페이지 이동 액션 설정 */}
|
||||||
|
{localSelects.actionType === "navigate" && (
|
||||||
|
<div className="mt-4 space-y-4 rounded-lg border bg-gray-50 p-4">
|
||||||
|
<h4 className="text-sm font-medium text-gray-700">페이지 이동 설정</h4>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="target-screen-nav">이동할 화면 선택</Label>
|
||||||
|
<Popover open={navScreenOpen} onOpenChange={setNavScreenOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded={navScreenOpen}
|
||||||
|
className="h-10 w-full justify-between"
|
||||||
|
disabled={screensLoading}
|
||||||
|
>
|
||||||
|
{config.action?.targetScreenId
|
||||||
|
? screens.find((screen) => screen.id === config.action?.targetScreenId)?.name ||
|
||||||
|
"화면을 선택하세요..."
|
||||||
|
: "화면을 선택하세요..."}
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="p-0" align="start" style={{ width: "var(--radix-popover-trigger-width)" }}>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex items-center border-b px-3 py-2">
|
||||||
|
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
<Input
|
||||||
|
placeholder="화면 검색..."
|
||||||
|
value={navSearchTerm}
|
||||||
|
onChange={(e) => setNavSearchTerm(e.target.value)}
|
||||||
|
className="border-0 p-0 focus-visible:ring-0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="max-h-[200px] overflow-auto">
|
||||||
|
{(() => {
|
||||||
|
const filteredScreens = filterScreens(navSearchTerm);
|
||||||
|
if (screensLoading) {
|
||||||
|
return <div className="p-3 text-sm text-gray-500">화면 목록을 불러오는 중...</div>;
|
||||||
|
}
|
||||||
|
if (filteredScreens.length === 0) {
|
||||||
|
return <div className="p-3 text-sm text-gray-500">검색 결과가 없습니다.</div>;
|
||||||
|
}
|
||||||
|
return filteredScreens.map((screen, index) => (
|
||||||
|
<div
|
||||||
|
key={`navigate-screen-${screen.id}-${index}`}
|
||||||
|
className="flex cursor-pointer items-center px-3 py-2 hover:bg-gray-100"
|
||||||
|
onClick={() => {
|
||||||
|
onUpdateProperty("componentConfig.action.targetScreenId", screen.id);
|
||||||
|
setNavScreenOpen(false);
|
||||||
|
setNavSearchTerm("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-4 w-4",
|
||||||
|
config.action?.targetScreenId === screen.id ? "opacity-100" : "opacity-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium">{screen.name}</span>
|
||||||
|
{screen.description && <span className="text-xs text-gray-500">{screen.description}</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<p className="mt-1 text-xs text-gray-500">
|
||||||
|
선택한 화면으로 /screens/{"{"}화면ID{"}"} 형태로 이동합니다
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="target-url">또는 직접 URL 입력 (고급)</Label>
|
||||||
|
<Input
|
||||||
|
id="target-url"
|
||||||
|
placeholder="예: /admin/users 또는 https://example.com"
|
||||||
|
value={localInputs.targetUrl}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newValue = e.target.value;
|
||||||
|
setLocalInputs((prev) => ({ ...prev, targetUrl: newValue }));
|
||||||
|
onUpdateProperty("componentConfig.action.targetUrl", newValue);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<p className="mt-1 text-xs text-gray-500">URL을 입력하면 화면 선택보다 우선 적용됩니다</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 🔥 NEW: 제어관리 기능 섹션 */}
|
||||||
|
<div className="mt-8 border-t border-gray-200 pt-6">
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900">🔧 고급 기능</h3>
|
||||||
|
<p className="text-muted-foreground mt-1 text-sm">버튼 액션과 함께 실행될 추가 기능을 설정합니다</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ImprovedButtonControlConfigPanel component={component} onUpdateProperty={onUpdateProperty} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect, useMemo } from "react";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
|
@ -26,25 +26,24 @@ interface ScreenOption {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component, onUpdateProperty }) => {
|
export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component, onUpdateProperty }) => {
|
||||||
|
console.log("🎨 ButtonConfigPanel 렌더링:", {
|
||||||
|
componentId: component.id,
|
||||||
|
"component.componentConfig?.action?.type": component.componentConfig?.action?.type,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 🔧 component에서 직접 읽기 (useMemo 제거)
|
||||||
const config = component.componentConfig || {};
|
const config = component.componentConfig || {};
|
||||||
|
const currentAction = component.componentConfig?.action || {};
|
||||||
|
|
||||||
// 로컬 상태 관리 (실시간 입력 반영)
|
// 로컬 상태 관리 (실시간 입력 반영)
|
||||||
const [localInputs, setLocalInputs] = useState({
|
const [localInputs, setLocalInputs] = useState({
|
||||||
text: config.text || "버튼",
|
text: config.text !== undefined ? config.text : "버튼",
|
||||||
modalTitle: config.action?.modalTitle || "",
|
modalTitle: config.action?.modalTitle || "",
|
||||||
editModalTitle: config.action?.editModalTitle || "",
|
editModalTitle: config.action?.editModalTitle || "",
|
||||||
editModalDescription: config.action?.editModalDescription || "",
|
editModalDescription: config.action?.editModalDescription || "",
|
||||||
targetUrl: config.action?.targetUrl || "",
|
targetUrl: config.action?.targetUrl || "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const [localSelects, setLocalSelects] = useState({
|
|
||||||
variant: config.variant || "default",
|
|
||||||
size: config.size || "default",
|
|
||||||
actionType: config.action?.type || "save",
|
|
||||||
modalSize: config.action?.modalSize || "md",
|
|
||||||
editMode: config.action?.editMode || "modal",
|
|
||||||
});
|
|
||||||
|
|
||||||
const [screens, setScreens] = useState<ScreenOption[]>([]);
|
const [screens, setScreens] = useState<ScreenOption[]>([]);
|
||||||
const [screensLoading, setScreensLoading] = useState(false);
|
const [screensLoading, setScreensLoading] = useState(false);
|
||||||
const [modalScreenOpen, setModalScreenOpen] = useState(false);
|
const [modalScreenOpen, setModalScreenOpen] = useState(false);
|
||||||
|
|
@ -52,44 +51,27 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
||||||
const [modalSearchTerm, setModalSearchTerm] = useState("");
|
const [modalSearchTerm, setModalSearchTerm] = useState("");
|
||||||
const [navSearchTerm, setNavSearchTerm] = useState("");
|
const [navSearchTerm, setNavSearchTerm] = useState("");
|
||||||
|
|
||||||
// 컴포넌트 변경 시 로컬 상태 동기화
|
// 컴포넌트 prop 변경 시 로컬 상태 동기화 (Input만)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLocalInputs({
|
const latestConfig = component.componentConfig || {};
|
||||||
text: config.text || "버튼",
|
const latestAction = latestConfig.action || {};
|
||||||
modalTitle: config.action?.modalTitle || "",
|
|
||||||
editModalTitle: config.action?.editModalTitle || "",
|
|
||||||
editModalDescription: config.action?.editModalDescription || "",
|
|
||||||
targetUrl: config.action?.targetUrl || "",
|
|
||||||
});
|
|
||||||
|
|
||||||
setLocalSelects({
|
setLocalInputs({
|
||||||
variant: config.variant || "default",
|
text: latestConfig.text !== undefined ? latestConfig.text : "버튼",
|
||||||
size: config.size || "default",
|
modalTitle: latestAction.modalTitle || "",
|
||||||
actionType: config.action?.type || "save",
|
editModalTitle: latestAction.editModalTitle || "",
|
||||||
modalSize: config.action?.modalSize || "md",
|
editModalDescription: latestAction.editModalDescription || "",
|
||||||
editMode: config.action?.editMode || "modal",
|
targetUrl: latestAction.targetUrl || "",
|
||||||
});
|
});
|
||||||
}, [
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
config.text,
|
}, [component.id]);
|
||||||
config.variant,
|
|
||||||
config.size,
|
|
||||||
config.action?.type,
|
|
||||||
config.action?.modalTitle,
|
|
||||||
config.action?.modalSize,
|
|
||||||
config.action?.editMode,
|
|
||||||
config.action?.editModalTitle,
|
|
||||||
config.action?.editModalDescription,
|
|
||||||
config.action?.targetUrl,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 화면 목록 가져오기
|
// 화면 목록 가져오기
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchScreens = async () => {
|
const fetchScreens = async () => {
|
||||||
try {
|
try {
|
||||||
setScreensLoading(true);
|
setScreensLoading(true);
|
||||||
// console.log("🔍 화면 목록 API 호출 시작");
|
|
||||||
const response = await apiClient.get("/screen-management/screens");
|
const response = await apiClient.get("/screen-management/screens");
|
||||||
// console.log("✅ 화면 목록 API 응답:", response.data);
|
|
||||||
|
|
||||||
if (response.data.success && Array.isArray(response.data.data)) {
|
if (response.data.success && Array.isArray(response.data.data)) {
|
||||||
const screenList = response.data.data.map((screen: any) => ({
|
const screenList = response.data.data.map((screen: any) => ({
|
||||||
|
|
@ -98,7 +80,6 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
||||||
description: screen.description,
|
description: screen.description,
|
||||||
}));
|
}));
|
||||||
setScreens(screenList);
|
setScreens(screenList);
|
||||||
// console.log("✅ 화면 목록 설정 완료:", screenList.length, "개");
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// console.error("❌ 화면 목록 로딩 실패:", error);
|
// console.error("❌ 화면 목록 로딩 실패:", error);
|
||||||
|
|
@ -120,13 +101,13 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("🔧 config-panels/ButtonConfigPanel 렌더링:", {
|
// console.log("🔧 config-panels/ButtonConfigPanel 렌더링:", {
|
||||||
component,
|
// component,
|
||||||
config,
|
// config,
|
||||||
action: config.action,
|
// action: config.action,
|
||||||
actionType: config.action?.type,
|
// actionType: config.action?.type,
|
||||||
screensCount: screens.length,
|
// screensCount: screens.length,
|
||||||
});
|
// });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
|
@ -147,9 +128,8 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="button-variant">버튼 스타일</Label>
|
<Label htmlFor="button-variant">버튼 스타일</Label>
|
||||||
<Select
|
<Select
|
||||||
value={localSelects.variant}
|
value={component.componentConfig?.variant || "default"}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
setLocalSelects((prev) => ({ ...prev, variant: value }));
|
|
||||||
onUpdateProperty("componentConfig.variant", value);
|
onUpdateProperty("componentConfig.variant", value);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -169,21 +149,20 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="button-size">버튼 크기</Label>
|
<Label htmlFor="button-size">버튼 글씨 크기</Label>
|
||||||
<Select
|
<Select
|
||||||
value={localSelects.size}
|
value={component.componentConfig?.size || "md"}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
setLocalSelects((prev) => ({ ...prev, size: value }));
|
|
||||||
onUpdateProperty("componentConfig.size", value);
|
onUpdateProperty("componentConfig.size", value);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="버튼 크기 선택" />
|
<SelectValue placeholder="버튼 글씨 크기 선택" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="small">작음 (Small)</SelectItem>
|
<SelectItem value="sm">작음 (Small)</SelectItem>
|
||||||
<SelectItem value="default">기본 (Default)</SelectItem>
|
<SelectItem value="md">기본 (Default)</SelectItem>
|
||||||
<SelectItem value="large">큼 (Large)</SelectItem>
|
<SelectItem value="lg">큼 (Large)</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -191,28 +170,23 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="button-action">버튼 액션</Label>
|
<Label htmlFor="button-action">버튼 액션</Label>
|
||||||
<Select
|
<Select
|
||||||
value={localSelects.actionType}
|
key={`action-${component.id}-${component.componentConfig?.action?.type || "save"}`}
|
||||||
|
value={component.componentConfig?.action?.type || "save"}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
console.log("🔵 버튼 액션 변경:", {
|
console.log("🎯 버튼 액션 드롭다운 변경:", {
|
||||||
oldValue: localSelects.actionType,
|
oldValue: component.componentConfig?.action?.type,
|
||||||
newValue: value,
|
newValue: value,
|
||||||
componentId: component.id,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 로컬 상태 업데이트
|
// 🔥 action.type 업데이트
|
||||||
setLocalSelects((prev) => ({ ...prev, actionType: value }));
|
|
||||||
|
|
||||||
// 액션 타입 업데이트
|
|
||||||
onUpdateProperty("componentConfig.action.type", value);
|
onUpdateProperty("componentConfig.action.type", value);
|
||||||
|
|
||||||
// 액션에 따른 라벨 색상 자동 설정 (별도 호출)
|
// 🔥 색상 업데이트는 충분히 지연 (React 리렌더링 완료 후)
|
||||||
if (value === "delete") {
|
setTimeout(() => {
|
||||||
// 삭제 액션일 때 빨간색으로 설정
|
const newColor = value === "delete" ? "#ef4444" : "#212121";
|
||||||
onUpdateProperty("style.labelColor", "#ef4444");
|
console.log("🎨 라벨 색상 업데이트:", { value, newColor });
|
||||||
} else {
|
onUpdateProperty("style.labelColor", newColor);
|
||||||
// 다른 액션일 때 기본색으로 리셋
|
}, 100); // 0 → 100ms로 증가
|
||||||
onUpdateProperty("style.labelColor", "#212121");
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
|
|
@ -236,7 +210,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 모달 열기 액션 설정 */}
|
{/* 모달 열기 액션 설정 */}
|
||||||
{localSelects.actionType === "modal" && (
|
{(component.componentConfig?.action?.type || "save") === "modal" && (
|
||||||
<div className="mt-4 space-y-4 rounded-lg border bg-gray-50 p-4">
|
<div className="mt-4 space-y-4 rounded-lg border bg-gray-50 p-4">
|
||||||
<h4 className="text-sm font-medium text-gray-700">모달 설정</h4>
|
<h4 className="text-sm font-medium text-gray-700">모달 설정</h4>
|
||||||
|
|
||||||
|
|
@ -249,10 +223,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const newValue = e.target.value;
|
const newValue = e.target.value;
|
||||||
setLocalInputs((prev) => ({ ...prev, modalTitle: newValue }));
|
setLocalInputs((prev) => ({ ...prev, modalTitle: newValue }));
|
||||||
onUpdateProperty("componentConfig.action", {
|
onUpdateProperty("componentConfig.action.modalTitle", newValue);
|
||||||
...config.action,
|
|
||||||
modalTitle: newValue,
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -260,13 +231,9 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="modal-size">모달 크기</Label>
|
<Label htmlFor="modal-size">모달 크기</Label>
|
||||||
<Select
|
<Select
|
||||||
value={localSelects.modalSize}
|
value={component.componentConfig?.action?.modalSize || "md"}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
setLocalSelects((prev) => ({ ...prev, modalSize: value }));
|
onUpdateProperty("componentConfig.action.modalSize", value);
|
||||||
onUpdateProperty("componentConfig.action", {
|
|
||||||
...config.action,
|
|
||||||
modalSize: value,
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
|
|
@ -301,7 +268,6 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="p-0" align="start" style={{ width: "var(--radix-popover-trigger-width)" }}>
|
<PopoverContent className="p-0" align="start" style={{ width: "var(--radix-popover-trigger-width)" }}>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
{/* 검색 입력 */}
|
|
||||||
<div className="flex items-center border-b px-3 py-2">
|
<div className="flex items-center border-b px-3 py-2">
|
||||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -311,7 +277,6 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
||||||
className="border-0 p-0 focus-visible:ring-0"
|
className="border-0 p-0 focus-visible:ring-0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* 검색 결과 */}
|
|
||||||
<div className="max-h-[200px] overflow-auto">
|
<div className="max-h-[200px] overflow-auto">
|
||||||
{(() => {
|
{(() => {
|
||||||
const filteredScreens = filterScreens(modalSearchTerm);
|
const filteredScreens = filterScreens(modalSearchTerm);
|
||||||
|
|
@ -326,10 +291,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
||||||
key={`modal-screen-${screen.id}-${index}`}
|
key={`modal-screen-${screen.id}-${index}`}
|
||||||
className="flex cursor-pointer items-center px-3 py-2 hover:bg-gray-100"
|
className="flex cursor-pointer items-center px-3 py-2 hover:bg-gray-100"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onUpdateProperty("componentConfig.action", {
|
onUpdateProperty("componentConfig.action.targetScreenId", screen.id);
|
||||||
...config.action,
|
|
||||||
targetScreenId: screen.id,
|
|
||||||
});
|
|
||||||
setModalScreenOpen(false);
|
setModalScreenOpen(false);
|
||||||
setModalSearchTerm("");
|
setModalSearchTerm("");
|
||||||
}}
|
}}
|
||||||
|
|
@ -356,7 +318,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 수정 액션 설정 */}
|
{/* 수정 액션 설정 */}
|
||||||
{localSelects.actionType === "edit" && (
|
{(component.componentConfig?.action?.type || "save") === "edit" && (
|
||||||
<div className="mt-4 space-y-4 rounded-lg border bg-green-50 p-4">
|
<div className="mt-4 space-y-4 rounded-lg border bg-green-50 p-4">
|
||||||
<h4 className="text-sm font-medium text-gray-700">수정 설정</h4>
|
<h4 className="text-sm font-medium text-gray-700">수정 설정</h4>
|
||||||
|
|
||||||
|
|
@ -380,7 +342,6 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="p-0" align="start" style={{ width: "var(--radix-popover-trigger-width)" }}>
|
<PopoverContent className="p-0" align="start" style={{ width: "var(--radix-popover-trigger-width)" }}>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
{/* 검색 입력 */}
|
|
||||||
<div className="flex items-center border-b px-3 py-2">
|
<div className="flex items-center border-b px-3 py-2">
|
||||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -390,7 +351,6 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
||||||
className="border-0 p-0 focus-visible:ring-0"
|
className="border-0 p-0 focus-visible:ring-0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* 검색 결과 */}
|
|
||||||
<div className="max-h-[200px] overflow-auto">
|
<div className="max-h-[200px] overflow-auto">
|
||||||
{(() => {
|
{(() => {
|
||||||
const filteredScreens = filterScreens(modalSearchTerm);
|
const filteredScreens = filterScreens(modalSearchTerm);
|
||||||
|
|
@ -405,10 +365,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
||||||
key={`edit-screen-${screen.id}-${index}`}
|
key={`edit-screen-${screen.id}-${index}`}
|
||||||
className="flex cursor-pointer items-center px-3 py-2 hover:bg-gray-100"
|
className="flex cursor-pointer items-center px-3 py-2 hover:bg-gray-100"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onUpdateProperty("componentConfig.action", {
|
onUpdateProperty("componentConfig.action.targetScreenId", screen.id);
|
||||||
...config.action,
|
|
||||||
targetScreenId: screen.id,
|
|
||||||
});
|
|
||||||
setModalScreenOpen(false);
|
setModalScreenOpen(false);
|
||||||
setModalSearchTerm("");
|
setModalSearchTerm("");
|
||||||
}}
|
}}
|
||||||
|
|
@ -438,13 +395,9 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="edit-mode">수정 모드</Label>
|
<Label htmlFor="edit-mode">수정 모드</Label>
|
||||||
<Select
|
<Select
|
||||||
value={localSelects.editMode}
|
value={component.componentConfig?.action?.editMode || "modal"}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
setLocalSelects((prev) => ({ ...prev, editMode: value }));
|
onUpdateProperty("componentConfig.action.editMode", value);
|
||||||
onUpdateProperty("componentConfig.action", {
|
|
||||||
...config.action,
|
|
||||||
editMode: value,
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
|
|
@ -458,7 +411,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{localSelects.editMode === "modal" && (
|
{(component.componentConfig?.action?.editMode || "modal") === "modal" && (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="edit-modal-title">모달 제목</Label>
|
<Label htmlFor="edit-modal-title">모달 제목</Label>
|
||||||
|
|
@ -469,11 +422,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const newValue = e.target.value;
|
const newValue = e.target.value;
|
||||||
setLocalInputs((prev) => ({ ...prev, editModalTitle: newValue }));
|
setLocalInputs((prev) => ({ ...prev, editModalTitle: newValue }));
|
||||||
onUpdateProperty("componentConfig.action", {
|
onUpdateProperty("componentConfig.action.editModalTitle", newValue);
|
||||||
...config.action,
|
|
||||||
editModalTitle: newValue,
|
|
||||||
});
|
|
||||||
// webTypeConfig에도 저장
|
|
||||||
onUpdateProperty("webTypeConfig.editModalTitle", newValue);
|
onUpdateProperty("webTypeConfig.editModalTitle", newValue);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
@ -489,11 +438,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const newValue = e.target.value;
|
const newValue = e.target.value;
|
||||||
setLocalInputs((prev) => ({ ...prev, editModalDescription: newValue }));
|
setLocalInputs((prev) => ({ ...prev, editModalDescription: newValue }));
|
||||||
onUpdateProperty("componentConfig.action", {
|
onUpdateProperty("componentConfig.action.editModalDescription", newValue);
|
||||||
...config.action,
|
|
||||||
editModalDescription: newValue,
|
|
||||||
});
|
|
||||||
// webTypeConfig에도 저장
|
|
||||||
onUpdateProperty("webTypeConfig.editModalDescription", newValue);
|
onUpdateProperty("webTypeConfig.editModalDescription", newValue);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
@ -503,13 +448,9 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="edit-modal-size">모달 크기</Label>
|
<Label htmlFor="edit-modal-size">모달 크기</Label>
|
||||||
<Select
|
<Select
|
||||||
value={localSelects.modalSize}
|
value={component.componentConfig?.action?.modalSize || "md"}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
setLocalSelects((prev) => ({ ...prev, modalSize: value }));
|
onUpdateProperty("componentConfig.action.modalSize", value);
|
||||||
onUpdateProperty("componentConfig.action", {
|
|
||||||
...config.action,
|
|
||||||
modalSize: value,
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
|
|
@ -530,7 +471,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 페이지 이동 액션 설정 */}
|
{/* 페이지 이동 액션 설정 */}
|
||||||
{localSelects.actionType === "navigate" && (
|
{(component.componentConfig?.action?.type || "save") === "navigate" && (
|
||||||
<div className="mt-4 space-y-4 rounded-lg border bg-gray-50 p-4">
|
<div className="mt-4 space-y-4 rounded-lg border bg-gray-50 p-4">
|
||||||
<h4 className="text-sm font-medium text-gray-700">페이지 이동 설정</h4>
|
<h4 className="text-sm font-medium text-gray-700">페이지 이동 설정</h4>
|
||||||
|
|
||||||
|
|
@ -554,7 +495,6 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="p-0" align="start" style={{ width: "var(--radix-popover-trigger-width)" }}>
|
<PopoverContent className="p-0" align="start" style={{ width: "var(--radix-popover-trigger-width)" }}>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
{/* 검색 입력 */}
|
|
||||||
<div className="flex items-center border-b px-3 py-2">
|
<div className="flex items-center border-b px-3 py-2">
|
||||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -564,7 +504,6 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
||||||
className="border-0 p-0 focus-visible:ring-0"
|
className="border-0 p-0 focus-visible:ring-0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* 검색 결과 */}
|
|
||||||
<div className="max-h-[200px] overflow-auto">
|
<div className="max-h-[200px] overflow-auto">
|
||||||
{(() => {
|
{(() => {
|
||||||
const filteredScreens = filterScreens(navSearchTerm);
|
const filteredScreens = filterScreens(navSearchTerm);
|
||||||
|
|
@ -579,10 +518,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
||||||
key={`navigate-screen-${screen.id}-${index}`}
|
key={`navigate-screen-${screen.id}-${index}`}
|
||||||
className="flex cursor-pointer items-center px-3 py-2 hover:bg-gray-100"
|
className="flex cursor-pointer items-center px-3 py-2 hover:bg-gray-100"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onUpdateProperty("componentConfig.action", {
|
onUpdateProperty("componentConfig.action.targetScreenId", screen.id);
|
||||||
...config.action,
|
|
||||||
targetScreenId: screen.id,
|
|
||||||
});
|
|
||||||
setNavScreenOpen(false);
|
setNavScreenOpen(false);
|
||||||
setNavSearchTerm("");
|
setNavSearchTerm("");
|
||||||
}}
|
}}
|
||||||
|
|
@ -618,10 +554,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const newValue = e.target.value;
|
const newValue = e.target.value;
|
||||||
setLocalInputs((prev) => ({ ...prev, targetUrl: newValue }));
|
setLocalInputs((prev) => ({ ...prev, targetUrl: newValue }));
|
||||||
onUpdateProperty("componentConfig.action", {
|
onUpdateProperty("componentConfig.action.targetUrl", newValue);
|
||||||
...config.action,
|
|
||||||
targetUrl: newValue,
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-xs text-gray-500">URL을 입력하면 화면 선택보다 우선 적용됩니다</p>
|
<p className="mt-1 text-xs text-gray-500">URL을 입력하면 화면 선택보다 우선 적용됩니다</p>
|
||||||
|
|
@ -641,3 +574,4 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -822,7 +822,8 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
case "button":
|
case "button":
|
||||||
case "button-primary":
|
case "button-primary":
|
||||||
case "button-secondary":
|
case "button-secondary":
|
||||||
return <NewButtonConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
|
// 🔧 component.id만 key로 사용 (unmount 방지)
|
||||||
|
return <NewButtonConfigPanel key={selectedComponent.id} component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
|
||||||
|
|
||||||
case "card":
|
case "card":
|
||||||
return <CardConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
|
return <CardConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,8 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
case "button":
|
case "button":
|
||||||
case "button-primary":
|
case "button-primary":
|
||||||
case "button-secondary":
|
case "button-secondary":
|
||||||
return <ButtonConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
|
// 🔧 component.id만 key로 사용 (unmount 방지)
|
||||||
|
return <ButtonConfigPanel key={selectedComponent.id} component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
|
||||||
|
|
||||||
case "card":
|
case "card":
|
||||||
return <CardConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
|
return <CardConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
|
||||||
|
|
|
||||||
|
|
@ -491,7 +491,8 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
? "linear-gradient(135deg, #e5e7eb 0%, #d1d5db 100%)"
|
? "linear-gradient(135deg, #e5e7eb 0%, #d1d5db 100%)"
|
||||||
: `linear-gradient(135deg, ${buttonColor} 0%, ${buttonDarkColor} 100%)`,
|
: `linear-gradient(135deg, ${buttonColor} 0%, ${buttonDarkColor} 100%)`,
|
||||||
color: componentConfig.disabled ? "#9ca3af" : "white",
|
color: componentConfig.disabled ? "#9ca3af" : "white",
|
||||||
fontSize: "0.875rem",
|
// 🔧 크기 설정 적용 (sm/md/lg)
|
||||||
|
fontSize: componentConfig.size === "sm" ? "0.75rem" : componentConfig.size === "lg" ? "1rem" : "0.875rem",
|
||||||
fontWeight: "600",
|
fontWeight: "600",
|
||||||
cursor: componentConfig.disabled ? "not-allowed" : "pointer",
|
cursor: componentConfig.disabled ? "not-allowed" : "pointer",
|
||||||
outline: "none",
|
outline: "none",
|
||||||
|
|
@ -499,10 +500,10 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
padding: "0 1rem",
|
// 🔧 크기에 따른 패딩 조정
|
||||||
|
padding: componentConfig.size === "sm" ? "0 0.75rem" : componentConfig.size === "lg" ? "0 1.25rem" : "0 1rem",
|
||||||
margin: "0",
|
margin: "0",
|
||||||
lineHeight: "1.25",
|
lineHeight: "1.25",
|
||||||
minHeight: "2.25rem",
|
|
||||||
boxShadow: componentConfig.disabled ? "0 1px 2px 0 rgba(0, 0, 0, 0.05)" : `0 1px 3px 0 ${buttonColor}40`,
|
boxShadow: componentConfig.disabled ? "0 1px 2px 0 rgba(0, 0, 0, 0.05)" : `0 1px 3px 0 ${buttonColor}40`,
|
||||||
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
...(isInteractive && component.style ? component.style : {}),
|
...(isInteractive && component.style ? component.style : {}),
|
||||||
|
|
@ -511,7 +512,8 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
onDragStart={onDragStart}
|
onDragStart={onDragStart}
|
||||||
onDragEnd={onDragEnd}
|
onDragEnd={onDragEnd}
|
||||||
>
|
>
|
||||||
{processedConfig.text || component.label || "버튼"}
|
{/* 🔧 빈 문자열도 허용 (undefined일 때만 기본값 적용) */}
|
||||||
|
{processedConfig.text !== undefined ? processedConfig.text : component.label || "버튼"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue