버튼 문제 해결

This commit is contained in:
leeheejin 2025-10-21 17:32:54 +09:00
parent fa30763ae2
commit 10d112bd69
6 changed files with 862 additions and 234 deletions

View File

@ -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],
); );
// 컴포넌트 드래그 시작 // 컴포넌트 드래그 시작

View File

@ -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>
);
};

View File

@ -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>
); );
}; };

View File

@ -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} />;

View File

@ -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} />;

View File

@ -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>