Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feat/dashboard
This commit is contained in:
commit
7ec60bed6c
|
|
@ -34,16 +34,35 @@ export class RiskAlertCacheService {
|
|||
*/
|
||||
public startAutoRefresh(): void {
|
||||
console.log('🔄 리스크/알림 자동 갱신 시작 (10분 간격)');
|
||||
console.log(' - 기상특보: 즉시 호출');
|
||||
console.log(' - 교통사고/도로공사: 10분 후 첫 호출');
|
||||
|
||||
// 즉시 첫 갱신
|
||||
this.refreshCache();
|
||||
// 기상특보만 즉시 호출 (ITS API는 10분 후부터)
|
||||
this.refreshWeatherOnly();
|
||||
|
||||
// 10분마다 갱신 (600,000ms)
|
||||
// 10분마다 전체 갱신 (600,000ms)
|
||||
this.updateInterval = setInterval(() => {
|
||||
this.refreshCache();
|
||||
}, 10 * 60 * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 기상특보만 갱신 (재시작 시 사용)
|
||||
*/
|
||||
private async refreshWeatherOnly(): Promise<void> {
|
||||
try {
|
||||
console.log('🌤️ 기상특보만 즉시 갱신 중...');
|
||||
const weatherAlerts = await this.riskAlertService.getWeatherAlerts();
|
||||
|
||||
this.cachedAlerts = weatherAlerts;
|
||||
this.lastUpdated = new Date();
|
||||
|
||||
console.log(`✅ 기상특보 갱신 완료! (${weatherAlerts.length}건)`);
|
||||
} catch (error: any) {
|
||||
console.error('❌ 기상특보 갱신 실패:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 자동 갱신 중지
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -33,11 +33,11 @@ export default function RiskAlertWidget({ element }: RiskAlertWidgetProps) {
|
|||
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
|
||||
const [newAlertIds, setNewAlertIds] = useState<Set<string>>(new Set());
|
||||
|
||||
// 데이터 로드 (백엔드 통합 호출)
|
||||
// 데이터 로드 (백엔드 캐시 조회)
|
||||
const loadData = async () => {
|
||||
setIsRefreshing(true);
|
||||
try {
|
||||
// 백엔드 API 호출 (교통사고, 기상특보, 도로공사 통합)
|
||||
// 백엔드 API 호출 (캐시된 데이터)
|
||||
const response = await apiClient.get<{
|
||||
success: boolean;
|
||||
data: Alert[];
|
||||
|
|
@ -79,6 +79,48 @@ export default function RiskAlertWidget({ element }: RiskAlertWidgetProps) {
|
|||
}
|
||||
};
|
||||
|
||||
// 강제 새로고침 (실시간 API 호출)
|
||||
const forceRefresh = async () => {
|
||||
setIsRefreshing(true);
|
||||
try {
|
||||
// 강제 갱신 API 호출 (실시간 데이터)
|
||||
const response = await apiClient.post<{
|
||||
success: boolean;
|
||||
data: Alert[];
|
||||
count: number;
|
||||
message?: string;
|
||||
}>("/risk-alerts/refresh", {});
|
||||
|
||||
if (response.data.success && response.data.data) {
|
||||
const newData = response.data.data;
|
||||
|
||||
// 새로운 알림 감지
|
||||
const oldIds = new Set(alerts.map(a => a.id));
|
||||
const newIds = new Set<string>();
|
||||
newData.forEach(alert => {
|
||||
if (!oldIds.has(alert.id)) {
|
||||
newIds.add(alert.id);
|
||||
}
|
||||
});
|
||||
|
||||
setAlerts(newData);
|
||||
setNewAlertIds(newIds);
|
||||
setLastUpdated(new Date());
|
||||
|
||||
// 3초 후 새 알림 애니메이션 제거
|
||||
if (newIds.size > 0) {
|
||||
setTimeout(() => setNewAlertIds(new Set()), 3000);
|
||||
}
|
||||
} else {
|
||||
console.error("❌ 리스크 알림 강제 갱신 실패");
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("❌ 리스크 알림 강제 갱신 오류:", error.message);
|
||||
} finally {
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
// 1분마다 자동 새로고침 (60000ms)
|
||||
|
|
@ -156,7 +198,7 @@ export default function RiskAlertWidget({ element }: RiskAlertWidgetProps) {
|
|||
{lastUpdated.toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit' })}
|
||||
</span>
|
||||
)}
|
||||
<Button variant="ghost" size="sm" onClick={loadData} disabled={isRefreshing}>
|
||||
<Button variant="ghost" size="sm" onClick={forceRefresh} disabled={isRefreshing} title="실시간 데이터 갱신">
|
||||
<RefreshCw className={`h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`} />
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -420,37 +420,39 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
const targetComponent = layout.components.find((comp) => comp.id === componentId);
|
||||
const isLayoutComponent = targetComponent?.type === "layout";
|
||||
// 🔥 함수형 업데이트로 변경하여 최신 layout 사용
|
||||
setLayout((prevLayout) => {
|
||||
const targetComponent = prevLayout.components.find((comp) => comp.id === componentId);
|
||||
const isLayoutComponent = targetComponent?.type === "layout";
|
||||
|
||||
// 레이아웃 컴포넌트의 위치가 변경되는 경우 존에 속한 컴포넌트들도 함께 이동
|
||||
const positionDelta = { x: 0, y: 0 };
|
||||
if (isLayoutComponent && (path === "position.x" || path === "position.y" || path === "position")) {
|
||||
const oldPosition = targetComponent.position;
|
||||
let newPosition = { ...oldPosition };
|
||||
// 레이아웃 컴포넌트의 위치가 변경되는 경우 존에 속한 컴포넌트들도 함께 이동
|
||||
const positionDelta = { x: 0, y: 0 };
|
||||
if (isLayoutComponent && (path === "position.x" || path === "position.y" || path === "position")) {
|
||||
const oldPosition = targetComponent.position;
|
||||
let newPosition = { ...oldPosition };
|
||||
|
||||
if (path === "position.x") {
|
||||
newPosition.x = value;
|
||||
positionDelta.x = value - oldPosition.x;
|
||||
} else if (path === "position.y") {
|
||||
newPosition.y = value;
|
||||
positionDelta.y = value - oldPosition.y;
|
||||
} else if (path === "position") {
|
||||
newPosition = value;
|
||||
positionDelta.x = value.x - oldPosition.x;
|
||||
positionDelta.y = value.y - oldPosition.y;
|
||||
if (path === "position.x") {
|
||||
newPosition.x = value;
|
||||
positionDelta.x = value - oldPosition.x;
|
||||
} else if (path === "position.y") {
|
||||
newPosition.y = value;
|
||||
positionDelta.y = value - oldPosition.y;
|
||||
} else if (path === "position") {
|
||||
newPosition = value;
|
||||
positionDelta.x = value.x - oldPosition.x;
|
||||
positionDelta.y = value.y - oldPosition.y;
|
||||
}
|
||||
|
||||
console.log("📐 레이아웃 이동 감지:", {
|
||||
layoutId: componentId,
|
||||
oldPosition,
|
||||
newPosition,
|
||||
positionDelta,
|
||||
});
|
||||
}
|
||||
|
||||
console.log("📐 레이아웃 이동 감지:", {
|
||||
layoutId: componentId,
|
||||
oldPosition,
|
||||
newPosition,
|
||||
positionDelta,
|
||||
});
|
||||
}
|
||||
|
||||
const pathParts = path.split(".");
|
||||
const updatedComponents = layout.components.map((comp) => {
|
||||
const pathParts = path.split(".");
|
||||
const updatedComponents = prevLayout.components.map((comp) => {
|
||||
if (comp.id !== componentId) {
|
||||
// 레이아웃 이동 시 존에 속한 컴포넌트들도 함께 이동
|
||||
if (isLayoutComponent && (positionDelta.x !== 0 || positionDelta.y !== 0)) {
|
||||
|
|
@ -480,22 +482,35 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
// 중첩 경로를 고려한 안전한 복사
|
||||
const newComp = { ...comp };
|
||||
|
||||
console.log("🔍 업데이트 전 상태:", {
|
||||
path,
|
||||
value,
|
||||
"기존 componentConfig": newComp.componentConfig,
|
||||
"기존 action": (newComp as any).componentConfig?.action,
|
||||
});
|
||||
|
||||
// 경로를 따라 내려가면서 각 레벨을 새 객체로 복사
|
||||
let current: any = newComp;
|
||||
for (let i = 0; i < pathParts.length - 1; 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])) {
|
||||
console.log(`🆕 새 객체 생성: ${key}`);
|
||||
current[key] = {};
|
||||
} else {
|
||||
// 기존 객체를 복사하여 불변성 유지
|
||||
console.log(`📋 기존 객체 복사: ${key}`, { ...current[key] });
|
||||
current[key] = { ...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("✅ 컴포넌트 업데이트 완료:", {
|
||||
componentId,
|
||||
|
|
@ -551,25 +566,25 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
// 크기 변경 시 격자 스냅 적용 (그룹 컴포넌트 제외)
|
||||
if (
|
||||
(path === "size.width" || path === "size.height") &&
|
||||
layout.gridSettings?.snapToGrid &&
|
||||
prevLayout.gridSettings?.snapToGrid &&
|
||||
gridInfo &&
|
||||
newComp.type !== "group"
|
||||
) {
|
||||
// 현재 해상도에 맞는 격자 정보로 스냅 적용
|
||||
const currentGridInfo = calculateGridInfo(screenResolution.width, screenResolution.height, {
|
||||
columns: layout.gridSettings.columns,
|
||||
gap: layout.gridSettings.gap,
|
||||
padding: layout.gridSettings.padding,
|
||||
snapToGrid: layout.gridSettings.snapToGrid || false,
|
||||
columns: prevLayout.gridSettings.columns,
|
||||
gap: prevLayout.gridSettings.gap,
|
||||
padding: prevLayout.gridSettings.padding,
|
||||
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;
|
||||
|
||||
// 크기 변경 시 gridColumns도 자동 조정
|
||||
const adjustedColumns = adjustGridColumnsFromSize(
|
||||
newComp,
|
||||
currentGridInfo,
|
||||
layout.gridSettings as GridUtilSettings,
|
||||
prevLayout.gridSettings as GridUtilSettings,
|
||||
);
|
||||
if (newComp.gridColumns !== adjustedColumns) {
|
||||
newComp.gridColumns = adjustedColumns;
|
||||
|
|
@ -582,19 +597,19 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
}
|
||||
|
||||
// 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, {
|
||||
columns: layout.gridSettings.columns,
|
||||
gap: layout.gridSettings.gap,
|
||||
padding: layout.gridSettings.padding,
|
||||
snapToGrid: layout.gridSettings.snapToGrid || false,
|
||||
columns: prevLayout.gridSettings.columns,
|
||||
gap: prevLayout.gridSettings.gap,
|
||||
padding: prevLayout.gridSettings.padding,
|
||||
snapToGrid: prevLayout.gridSettings.snapToGrid || false,
|
||||
});
|
||||
|
||||
// gridColumns에 맞는 정확한 너비 계산
|
||||
const newWidth = calculateWidthFromColumns(
|
||||
newComp.gridColumns,
|
||||
currentGridInfo,
|
||||
layout.gridSettings as GridUtilSettings,
|
||||
prevLayout.gridSettings as GridUtilSettings,
|
||||
);
|
||||
newComp.size = {
|
||||
...newComp.size,
|
||||
|
|
@ -699,52 +714,71 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
}
|
||||
}
|
||||
|
||||
return newComp;
|
||||
});
|
||||
return newComp;
|
||||
});
|
||||
|
||||
const newLayout = { ...layout, components: updatedComponents };
|
||||
setLayout(newLayout);
|
||||
saveToHistory(newLayout);
|
||||
// 🔥 새로운 layout 생성
|
||||
const newLayout = { ...prevLayout, components: updatedComponents };
|
||||
|
||||
console.log("🔄 setLayout 실행:", {
|
||||
componentId,
|
||||
path,
|
||||
value,
|
||||
업데이트된컴포넌트: updatedComponents.find((c) => c.id === componentId),
|
||||
});
|
||||
|
||||
saveToHistory(newLayout);
|
||||
|
||||
// selectedComponent가 업데이트된 컴포넌트와 같다면 selectedComponent도 업데이트
|
||||
setSelectedComponent((prevSelected) => {
|
||||
if (prevSelected && prevSelected.id === componentId) {
|
||||
const updatedSelectedComponent = updatedComponents.find((c) => c.id === componentId);
|
||||
if (updatedSelectedComponent) {
|
||||
// 🔧 완전히 새로운 객체를 만들어서 React가 변경을 감지하도록 함
|
||||
const newSelectedComponent = JSON.parse(JSON.stringify(updatedSelectedComponent));
|
||||
|
||||
console.log("🔄 selectedComponent 동기화:", {
|
||||
componentId,
|
||||
path,
|
||||
oldAction: (prevSelected as any).componentConfig?.action,
|
||||
newAction: (newSelectedComponent as any).componentConfig?.action,
|
||||
oldColumnsCount:
|
||||
prevSelected.type === "datatable" ? (prevSelected as any).columns?.length : "N/A",
|
||||
newColumnsCount:
|
||||
newSelectedComponent.type === "datatable" ? (newSelectedComponent as any).columns?.length : "N/A",
|
||||
oldFiltersCount:
|
||||
prevSelected.type === "datatable" ? (prevSelected as any).filters?.length : "N/A",
|
||||
newFiltersCount:
|
||||
newSelectedComponent.type === "datatable" ? (newSelectedComponent as any).filters?.length : "N/A",
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
return newSelectedComponent;
|
||||
}
|
||||
}
|
||||
return prevSelected;
|
||||
});
|
||||
|
||||
// selectedComponent가 업데이트된 컴포넌트와 같다면 selectedComponent도 업데이트
|
||||
if (selectedComponent && selectedComponent.id === componentId) {
|
||||
const updatedSelectedComponent = updatedComponents.find((c) => c.id === componentId);
|
||||
if (updatedSelectedComponent) {
|
||||
console.log("🔄 selectedComponent 동기화:", {
|
||||
// webTypeConfig 업데이트 후 레이아웃 상태 확인
|
||||
if (path === "webTypeConfig") {
|
||||
const updatedComponent = newLayout.components.find((c) => c.id === componentId);
|
||||
console.log("🔄 레이아웃 업데이트 후 컴포넌트 상태:", {
|
||||
componentId,
|
||||
path,
|
||||
oldColumnsCount:
|
||||
selectedComponent.type === "datatable" ? (selectedComponent as any).columns?.length : "N/A",
|
||||
newColumnsCount:
|
||||
updatedSelectedComponent.type === "datatable" ? (updatedSelectedComponent as any).columns?.length : "N/A",
|
||||
oldFiltersCount:
|
||||
selectedComponent.type === "datatable" ? (selectedComponent as any).filters?.length : "N/A",
|
||||
newFiltersCount:
|
||||
updatedSelectedComponent.type === "datatable" ? (updatedSelectedComponent as any).filters?.length : "N/A",
|
||||
updatedComponent: updatedComponent
|
||||
? {
|
||||
id: updatedComponent.id,
|
||||
type: updatedComponent.type,
|
||||
webTypeConfig: updatedComponent.type === "widget" ? (updatedComponent as any).webTypeConfig : null,
|
||||
}
|
||||
: null,
|
||||
layoutComponentsCount: newLayout.components.length,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
setSelectedComponent(updatedSelectedComponent);
|
||||
}
|
||||
}
|
||||
|
||||
// webTypeConfig 업데이트 후 레이아웃 상태 확인
|
||||
if (path === "webTypeConfig") {
|
||||
const updatedComponent = newLayout.components.find((c) => c.id === componentId);
|
||||
console.log("🔄 레이아웃 업데이트 후 컴포넌트 상태:", {
|
||||
componentId,
|
||||
updatedComponent: updatedComponent
|
||||
? {
|
||||
id: updatedComponent.id,
|
||||
type: updatedComponent.type,
|
||||
webTypeConfig: updatedComponent.type === "widget" ? (updatedComponent as any).webTypeConfig : null,
|
||||
}
|
||||
: null,
|
||||
layoutComponentsCount: newLayout.components.length,
|
||||
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,
|
||||
screenResolution: screenResolution,
|
||||
};
|
||||
// 🔍 버튼 컴포넌트들의 action.type 확인
|
||||
const buttonComponents = layoutWithResolution.components.filter(
|
||||
(c: any) => c.type === "button" || c.type === "button-primary" || c.type === "button-secondary"
|
||||
);
|
||||
console.log("💾 저장 시작:", {
|
||||
screenId: selectedScreen.screenId,
|
||||
componentsCount: layoutWithResolution.components.length,
|
||||
gridSettings: layoutWithResolution.gridSettings,
|
||||
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);
|
||||
|
|
@ -2127,7 +2172,8 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
|
||||
// 새 컴포넌트 선택
|
||||
setSelectedComponent(newComponent);
|
||||
openPanel("properties");
|
||||
// 🔧 테이블 패널 유지를 위해 자동 속성 패널 열기 비활성화
|
||||
// openPanel("properties");
|
||||
|
||||
toast.success(`${component.name} 컴포넌트가 추가되었습니다.`);
|
||||
},
|
||||
|
|
@ -2610,8 +2656,8 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
saveToHistory(newLayout);
|
||||
setSelectedComponent(newComponent);
|
||||
|
||||
// 속성 패널 자동 열기
|
||||
openPanel("properties");
|
||||
// 🔧 테이블 패널 유지를 위해 자동 속성 패널 열기 비활성화
|
||||
// openPanel("properties");
|
||||
} catch (error) {
|
||||
// console.error("드롭 처리 실패:", error);
|
||||
}
|
||||
|
|
@ -2674,47 +2720,66 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
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 isCtrlPressed = event?.ctrlKey || event?.metaKey || false;
|
||||
const isGroupContainer = component.type === "group";
|
||||
const isGroupContainer = latestComponent.type === "group";
|
||||
|
||||
if (isShiftPressed || isCtrlPressed || groupState.isGrouping) {
|
||||
// 다중 선택 모드
|
||||
if (isGroupContainer) {
|
||||
// 그룹 컨테이너는 단일 선택으로 처리
|
||||
handleComponentSelect(component);
|
||||
handleComponentSelect(latestComponent); // 🔧 최신 버전 사용
|
||||
setGroupState((prev) => ({
|
||||
...prev,
|
||||
selectedComponents: [component.id],
|
||||
selectedComponents: [latestComponent.id],
|
||||
isGrouping: false,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
const isSelected = groupState.selectedComponents.includes(component.id);
|
||||
const isSelected = groupState.selectedComponents.includes(latestComponent.id);
|
||||
setGroupState((prev) => ({
|
||||
...prev,
|
||||
selectedComponents: isSelected
|
||||
? prev.selectedComponents.filter((id) => id !== component.id)
|
||||
: [...prev.selectedComponents, component.id],
|
||||
? prev.selectedComponents.filter((id) => id !== latestComponent.id)
|
||||
: [...prev.selectedComponents, latestComponent.id],
|
||||
}));
|
||||
|
||||
// 마지막 선택된 컴포넌트를 selectedComponent로 설정
|
||||
if (!isSelected) {
|
||||
// console.log("🎯 컴포넌트 선택 (다중 모드):", component.id);
|
||||
handleComponentSelect(component);
|
||||
// console.log("🎯 컴포넌트 선택 (다중 모드):", latestComponent.id);
|
||||
handleComponentSelect(latestComponent); // 🔧 최신 버전 사용
|
||||
}
|
||||
} else {
|
||||
// 단일 선택 모드
|
||||
// console.log("🎯 컴포넌트 선택 (단일 모드):", component.id);
|
||||
handleComponentSelect(component);
|
||||
// console.log("🎯 컴포넌트 선택 (단일 모드):", latestComponent.id);
|
||||
handleComponentSelect(latestComponent); // 🔧 최신 버전 사용
|
||||
setGroupState((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";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useState, useEffect, useMemo } from "react";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
|
|
@ -26,25 +26,24 @@ interface ScreenOption {
|
|||
}
|
||||
|
||||
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 currentAction = component.componentConfig?.action || {};
|
||||
|
||||
// 로컬 상태 관리 (실시간 입력 반영)
|
||||
const [localInputs, setLocalInputs] = useState({
|
||||
text: config.text || "버튼",
|
||||
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 || "default",
|
||||
actionType: config.action?.type || "save",
|
||||
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);
|
||||
|
|
@ -52,44 +51,27 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
|||
const [modalSearchTerm, setModalSearchTerm] = useState("");
|
||||
const [navSearchTerm, setNavSearchTerm] = useState("");
|
||||
|
||||
// 컴포넌트 변경 시 로컬 상태 동기화
|
||||
// 컴포넌트 prop 변경 시 로컬 상태 동기화 (Input만)
|
||||
useEffect(() => {
|
||||
setLocalInputs({
|
||||
text: config.text || "버튼",
|
||||
modalTitle: config.action?.modalTitle || "",
|
||||
editModalTitle: config.action?.editModalTitle || "",
|
||||
editModalDescription: config.action?.editModalDescription || "",
|
||||
targetUrl: config.action?.targetUrl || "",
|
||||
});
|
||||
const latestConfig = component.componentConfig || {};
|
||||
const latestAction = latestConfig.action || {};
|
||||
|
||||
setLocalSelects({
|
||||
variant: config.variant || "default",
|
||||
size: config.size || "default",
|
||||
actionType: config.action?.type || "save",
|
||||
modalSize: config.action?.modalSize || "md",
|
||||
editMode: config.action?.editMode || "modal",
|
||||
setLocalInputs({
|
||||
text: latestConfig.text !== undefined ? latestConfig.text : "버튼",
|
||||
modalTitle: latestAction.modalTitle || "",
|
||||
editModalTitle: latestAction.editModalTitle || "",
|
||||
editModalDescription: latestAction.editModalDescription || "",
|
||||
targetUrl: latestAction.targetUrl || "",
|
||||
});
|
||||
}, [
|
||||
config.text,
|
||||
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,
|
||||
]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [component.id]);
|
||||
|
||||
// 화면 목록 가져오기
|
||||
useEffect(() => {
|
||||
const fetchScreens = async () => {
|
||||
try {
|
||||
setScreensLoading(true);
|
||||
// console.log("🔍 화면 목록 API 호출 시작");
|
||||
const response = await apiClient.get("/screen-management/screens");
|
||||
// console.log("✅ 화면 목록 API 응답:", response.data);
|
||||
|
||||
if (response.data.success && Array.isArray(response.data.data)) {
|
||||
const screenList = response.data.data.map((screen: any) => ({
|
||||
|
|
@ -98,7 +80,6 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
|||
description: screen.description,
|
||||
}));
|
||||
setScreens(screenList);
|
||||
// console.log("✅ 화면 목록 설정 완료:", screenList.length, "개");
|
||||
}
|
||||
} catch (error) {
|
||||
// console.error("❌ 화면 목록 로딩 실패:", error);
|
||||
|
|
@ -120,13 +101,13 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
|||
);
|
||||
};
|
||||
|
||||
console.log("🔧 config-panels/ButtonConfigPanel 렌더링:", {
|
||||
component,
|
||||
config,
|
||||
action: config.action,
|
||||
actionType: config.action?.type,
|
||||
screensCount: screens.length,
|
||||
});
|
||||
// console.log("🔧 config-panels/ButtonConfigPanel 렌더링:", {
|
||||
// component,
|
||||
// config,
|
||||
// action: config.action,
|
||||
// actionType: config.action?.type,
|
||||
// screensCount: screens.length,
|
||||
// });
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
|
|
@ -147,9 +128,8 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
|||
<div>
|
||||
<Label htmlFor="button-variant">버튼 스타일</Label>
|
||||
<Select
|
||||
value={localSelects.variant}
|
||||
value={component.componentConfig?.variant || "default"}
|
||||
onValueChange={(value) => {
|
||||
setLocalSelects((prev) => ({ ...prev, variant: value }));
|
||||
onUpdateProperty("componentConfig.variant", value);
|
||||
}}
|
||||
>
|
||||
|
|
@ -169,21 +149,20 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="button-size">버튼 크기</Label>
|
||||
<Label htmlFor="button-size">버튼 글씨 크기</Label>
|
||||
<Select
|
||||
value={localSelects.size}
|
||||
value={component.componentConfig?.size || "md"}
|
||||
onValueChange={(value) => {
|
||||
setLocalSelects((prev) => ({ ...prev, size: value }));
|
||||
onUpdateProperty("componentConfig.size", value);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="버튼 크기 선택" />
|
||||
<SelectValue placeholder="버튼 글씨 크기 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="small">작음 (Small)</SelectItem>
|
||||
<SelectItem value="default">기본 (Default)</SelectItem>
|
||||
<SelectItem value="large">큼 (Large)</SelectItem>
|
||||
<SelectItem value="sm">작음 (Small)</SelectItem>
|
||||
<SelectItem value="md">기본 (Default)</SelectItem>
|
||||
<SelectItem value="lg">큼 (Large)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
|
@ -191,28 +170,23 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
|||
<div>
|
||||
<Label htmlFor="button-action">버튼 액션</Label>
|
||||
<Select
|
||||
value={localSelects.actionType}
|
||||
key={`action-${component.id}-${component.componentConfig?.action?.type || "save"}`}
|
||||
value={component.componentConfig?.action?.type || "save"}
|
||||
onValueChange={(value) => {
|
||||
console.log("🔵 버튼 액션 변경:", {
|
||||
oldValue: localSelects.actionType,
|
||||
newValue: value,
|
||||
componentId: component.id,
|
||||
});
|
||||
|
||||
// 로컬 상태 업데이트
|
||||
setLocalSelects((prev) => ({ ...prev, actionType: value }));
|
||||
|
||||
// 액션 타입 업데이트
|
||||
onUpdateProperty("componentConfig.action.type", value);
|
||||
|
||||
// 액션에 따른 라벨 색상 자동 설정 (별도 호출)
|
||||
if (value === "delete") {
|
||||
// 삭제 액션일 때 빨간색으로 설정
|
||||
onUpdateProperty("style.labelColor", "#ef4444");
|
||||
} else {
|
||||
// 다른 액션일 때 기본색으로 리셋
|
||||
onUpdateProperty("style.labelColor", "#212121");
|
||||
}
|
||||
console.log("🎯 버튼 액션 드롭다운 변경:", {
|
||||
oldValue: component.componentConfig?.action?.type,
|
||||
newValue: value,
|
||||
});
|
||||
|
||||
// 🔥 action.type 업데이트
|
||||
onUpdateProperty("componentConfig.action.type", value);
|
||||
|
||||
// 🔥 색상 업데이트는 충분히 지연 (React 리렌더링 완료 후)
|
||||
setTimeout(() => {
|
||||
const newColor = value === "delete" ? "#ef4444" : "#212121";
|
||||
console.log("🎨 라벨 색상 업데이트:", { value, newColor });
|
||||
onUpdateProperty("style.labelColor", newColor);
|
||||
}, 100); // 0 → 100ms로 증가
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
|
|
@ -236,7 +210,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
|||
</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">
|
||||
<h4 className="text-sm font-medium text-gray-700">모달 설정</h4>
|
||||
|
||||
|
|
@ -249,10 +223,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
|||
onChange={(e) => {
|
||||
const newValue = e.target.value;
|
||||
setLocalInputs((prev) => ({ ...prev, modalTitle: newValue }));
|
||||
onUpdateProperty("componentConfig.action", {
|
||||
...config.action,
|
||||
modalTitle: newValue,
|
||||
});
|
||||
onUpdateProperty("componentConfig.action.modalTitle", newValue);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -260,13 +231,9 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
|||
<div>
|
||||
<Label htmlFor="modal-size">모달 크기</Label>
|
||||
<Select
|
||||
value={localSelects.modalSize}
|
||||
value={component.componentConfig?.action?.modalSize || "md"}
|
||||
onValueChange={(value) => {
|
||||
setLocalSelects((prev) => ({ ...prev, modalSize: value }));
|
||||
onUpdateProperty("componentConfig.action", {
|
||||
...config.action,
|
||||
modalSize: value,
|
||||
});
|
||||
onUpdateProperty("componentConfig.action.modalSize", value);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
|
|
@ -301,7 +268,6 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
|||
</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
|
||||
|
|
@ -311,7 +277,6 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
|||
className="border-0 p-0 focus-visible:ring-0"
|
||||
/>
|
||||
</div>
|
||||
{/* 검색 결과 */}
|
||||
<div className="max-h-[200px] overflow-auto">
|
||||
{(() => {
|
||||
const filteredScreens = filterScreens(modalSearchTerm);
|
||||
|
|
@ -326,10 +291,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
|||
key={`modal-screen-${screen.id}-${index}`}
|
||||
className="flex cursor-pointer items-center px-3 py-2 hover:bg-gray-100"
|
||||
onClick={() => {
|
||||
onUpdateProperty("componentConfig.action", {
|
||||
...config.action,
|
||||
targetScreenId: screen.id,
|
||||
});
|
||||
onUpdateProperty("componentConfig.action.targetScreenId", screen.id);
|
||||
setModalScreenOpen(false);
|
||||
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">
|
||||
<h4 className="text-sm font-medium text-gray-700">수정 설정</h4>
|
||||
|
||||
|
|
@ -380,7 +342,6 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
|||
</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
|
||||
|
|
@ -390,7 +351,6 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
|||
className="border-0 p-0 focus-visible:ring-0"
|
||||
/>
|
||||
</div>
|
||||
{/* 검색 결과 */}
|
||||
<div className="max-h-[200px] overflow-auto">
|
||||
{(() => {
|
||||
const filteredScreens = filterScreens(modalSearchTerm);
|
||||
|
|
@ -405,10 +365,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
|||
key={`edit-screen-${screen.id}-${index}`}
|
||||
className="flex cursor-pointer items-center px-3 py-2 hover:bg-gray-100"
|
||||
onClick={() => {
|
||||
onUpdateProperty("componentConfig.action", {
|
||||
...config.action,
|
||||
targetScreenId: screen.id,
|
||||
});
|
||||
onUpdateProperty("componentConfig.action.targetScreenId", screen.id);
|
||||
setModalScreenOpen(false);
|
||||
setModalSearchTerm("");
|
||||
}}
|
||||
|
|
@ -438,13 +395,9 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
|||
<div>
|
||||
<Label htmlFor="edit-mode">수정 모드</Label>
|
||||
<Select
|
||||
value={localSelects.editMode}
|
||||
value={component.componentConfig?.action?.editMode || "modal"}
|
||||
onValueChange={(value) => {
|
||||
setLocalSelects((prev) => ({ ...prev, editMode: value }));
|
||||
onUpdateProperty("componentConfig.action", {
|
||||
...config.action,
|
||||
editMode: value,
|
||||
});
|
||||
onUpdateProperty("componentConfig.action.editMode", value);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
|
|
@ -458,7 +411,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
|||
</Select>
|
||||
</div>
|
||||
|
||||
{localSelects.editMode === "modal" && (
|
||||
{(component.componentConfig?.action?.editMode || "modal") === "modal" && (
|
||||
<>
|
||||
<div>
|
||||
<Label htmlFor="edit-modal-title">모달 제목</Label>
|
||||
|
|
@ -469,11 +422,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
|||
onChange={(e) => {
|
||||
const newValue = e.target.value;
|
||||
setLocalInputs((prev) => ({ ...prev, editModalTitle: newValue }));
|
||||
onUpdateProperty("componentConfig.action", {
|
||||
...config.action,
|
||||
editModalTitle: newValue,
|
||||
});
|
||||
// webTypeConfig에도 저장
|
||||
onUpdateProperty("componentConfig.action.editModalTitle", newValue);
|
||||
onUpdateProperty("webTypeConfig.editModalTitle", newValue);
|
||||
}}
|
||||
/>
|
||||
|
|
@ -489,11 +438,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
|||
onChange={(e) => {
|
||||
const newValue = e.target.value;
|
||||
setLocalInputs((prev) => ({ ...prev, editModalDescription: newValue }));
|
||||
onUpdateProperty("componentConfig.action", {
|
||||
...config.action,
|
||||
editModalDescription: newValue,
|
||||
});
|
||||
// webTypeConfig에도 저장
|
||||
onUpdateProperty("componentConfig.action.editModalDescription", newValue);
|
||||
onUpdateProperty("webTypeConfig.editModalDescription", newValue);
|
||||
}}
|
||||
/>
|
||||
|
|
@ -503,13 +448,9 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
|||
<div>
|
||||
<Label htmlFor="edit-modal-size">모달 크기</Label>
|
||||
<Select
|
||||
value={localSelects.modalSize}
|
||||
value={component.componentConfig?.action?.modalSize || "md"}
|
||||
onValueChange={(value) => {
|
||||
setLocalSelects((prev) => ({ ...prev, modalSize: value }));
|
||||
onUpdateProperty("componentConfig.action", {
|
||||
...config.action,
|
||||
modalSize: value,
|
||||
});
|
||||
onUpdateProperty("componentConfig.action.modalSize", value);
|
||||
}}
|
||||
>
|
||||
<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">
|
||||
<h4 className="text-sm font-medium text-gray-700">페이지 이동 설정</h4>
|
||||
|
||||
|
|
@ -554,7 +495,6 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
|||
</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
|
||||
|
|
@ -564,7 +504,6 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
|||
className="border-0 p-0 focus-visible:ring-0"
|
||||
/>
|
||||
</div>
|
||||
{/* 검색 결과 */}
|
||||
<div className="max-h-[200px] overflow-auto">
|
||||
{(() => {
|
||||
const filteredScreens = filterScreens(navSearchTerm);
|
||||
|
|
@ -579,10 +518,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
|||
key={`navigate-screen-${screen.id}-${index}`}
|
||||
className="flex cursor-pointer items-center px-3 py-2 hover:bg-gray-100"
|
||||
onClick={() => {
|
||||
onUpdateProperty("componentConfig.action", {
|
||||
...config.action,
|
||||
targetScreenId: screen.id,
|
||||
});
|
||||
onUpdateProperty("componentConfig.action.targetScreenId", screen.id);
|
||||
setNavScreenOpen(false);
|
||||
setNavSearchTerm("");
|
||||
}}
|
||||
|
|
@ -618,10 +554,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
|||
onChange={(e) => {
|
||||
const newValue = e.target.value;
|
||||
setLocalInputs((prev) => ({ ...prev, targetUrl: newValue }));
|
||||
onUpdateProperty("componentConfig.action", {
|
||||
...config.action,
|
||||
targetUrl: newValue,
|
||||
});
|
||||
onUpdateProperty("componentConfig.action.targetUrl", newValue);
|
||||
}}
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500">URL을 입력하면 화면 선택보다 우선 적용됩니다</p>
|
||||
|
|
@ -641,3 +574,4 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
|||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -822,7 +822,8 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
|||
case "button":
|
||||
case "button-primary":
|
||||
case "button-secondary":
|
||||
return <NewButtonConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
|
||||
// 🔧 component.id만 key로 사용 (unmount 방지)
|
||||
return <NewButtonConfigPanel key={selectedComponent.id} component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
|
||||
|
||||
case "card":
|
||||
return <CardConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
|
||||
|
|
|
|||
|
|
@ -123,7 +123,8 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
|||
case "button":
|
||||
case "button-primary":
|
||||
case "button-secondary":
|
||||
return <ButtonConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
|
||||
// 🔧 component.id만 key로 사용 (unmount 방지)
|
||||
return <ButtonConfigPanel key={selectedComponent.id} component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
|
||||
|
||||
case "card":
|
||||
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, ${buttonColor} 0%, ${buttonDarkColor} 100%)`,
|
||||
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",
|
||||
cursor: componentConfig.disabled ? "not-allowed" : "pointer",
|
||||
outline: "none",
|
||||
|
|
@ -499,10 +500,10 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
|||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: "0 1rem",
|
||||
// 🔧 크기에 따른 패딩 조정
|
||||
padding: componentConfig.size === "sm" ? "0 0.75rem" : componentConfig.size === "lg" ? "0 1.25rem" : "0 1rem",
|
||||
margin: "0",
|
||||
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`,
|
||||
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||
...(isInteractive && component.style ? component.style : {}),
|
||||
|
|
@ -511,7 +512,8 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
|||
onDragStart={onDragStart}
|
||||
onDragEnd={onDragEnd}
|
||||
>
|
||||
{processedConfig.text || component.label || "버튼"}
|
||||
{/* 🔧 빈 문자열도 허용 (undefined일 때만 기본값 적용) */}
|
||||
{processedConfig.text !== undefined ? processedConfig.text : component.label || "버튼"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue