dev #46

Merged
kjs merged 344 commits from dev into main 2025-09-22 18:17:24 +09:00
3 changed files with 194 additions and 36 deletions
Showing only changes of commit 3bf694ce24 - Show all commits

View File

@ -1762,6 +1762,12 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
grabOffsetY: relativeMouseY - component.position.y,
});
console.log("🚀 드래그 시작:", {
componentId: component.id,
componentType: component.type,
initialPosition: { x: component.position.x, y: component.position.y },
});
setDragState({
isDragging: true,
draggedComponent: component, // 주 드래그 컴포넌트 (마우스 위치 기준)
@ -1804,13 +1810,30 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
};
// 드래그 상태 업데이트
setDragState((prev) => ({
...prev,
currentPosition: newPosition,
}));
console.log("🔥 ScreenDesigner updateDragPosition:", {
draggedComponentId: dragState.draggedComponent.id,
oldPosition: dragState.currentPosition,
newPosition: newPosition,
});
// 실시간 피드백은 렌더링에서 처리하므로 setLayout 호출 제거
// 성능 최적화: 드래그 중에는 상태 업데이트만 하고, 실제 레이아웃 업데이트는 endDrag에서 처리
setDragState((prev) => {
const newState = {
...prev,
currentPosition: { ...newPosition }, // 새로운 객체 생성
};
console.log("🔄 ScreenDesigner dragState 업데이트:", {
prevPosition: prev.currentPosition,
newPosition: newState.currentPosition,
stateChanged:
prev.currentPosition.x !== newState.currentPosition.x ||
prev.currentPosition.y !== newState.currentPosition.y,
});
return newState;
});
// 성능 최적화: 드래그 중에는 상태 업데이트만 하고,
// 실제 레이아웃 업데이트는 endDrag에서 처리
// 속성 패널에서는 dragState.currentPosition을 참조하여 실시간 표시
},
[dragState.isDragging, dragState.draggedComponent, dragState.grabOffset],
);
@ -1950,6 +1973,19 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
const newLayout = { ...layout, components: updatedComponents };
setLayout(newLayout);
// 선택된 컴포넌트도 업데이트 (PropertiesPanel 동기화용)
if (selectedComponent && dragState.draggedComponents.some((c) => c.id === selectedComponent.id)) {
const updatedSelectedComponent = updatedComponents.find((c) => c.id === selectedComponent.id);
if (updatedSelectedComponent) {
console.log("🔄 ScreenDesigner: 선택된 컴포넌트 위치 업데이트", {
componentId: selectedComponent.id,
oldPosition: selectedComponent.position,
newPosition: updatedSelectedComponent.position,
});
setSelectedComponent(updatedSelectedComponent);
}
}
// 히스토리에 저장
saveToHistory(newLayout);
}
@ -3123,8 +3159,10 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
autoHeight={true}
>
<PropertiesPanel
key={`properties-${selectedComponent?.id}-${dragState.isDragging ? dragState.currentPosition.x + dragState.currentPosition.y : "static"}`}
selectedComponent={selectedComponent || undefined}
tables={tables}
dragState={dragState}
onUpdateProperty={(path: string, value: any) => {
console.log("🔧 속성 업데이트 요청:", {
componentId: selectedComponent?.id,

View File

@ -94,6 +94,11 @@ const DataTableConfigPanelWrapper: React.FC<{
interface PropertiesPanelProps {
selectedComponent?: ComponentData;
tables?: TableInfo[];
dragState?: {
isDragging: boolean;
draggedComponent: ComponentData | null;
currentPosition: { x: number; y: number; z: number };
};
onUpdateProperty: (path: string, value: unknown) => void;
onDeleteComponent: () => void;
onCopyComponent: () => void;
@ -108,6 +113,7 @@ interface PropertiesPanelProps {
const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
selectedComponent,
tables = [],
dragState,
onUpdateProperty,
onDeleteComponent,
onCopyComponent,
@ -116,9 +122,29 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
canGroup = false,
canUngroup = false,
}) => {
// 🔍 디버깅: PropertiesPanel 렌더링 및 dragState 전달 확인
console.log("📍 PropertiesPanel 렌더링:", {
renderTime: Date.now(),
selectedComponentId: selectedComponent?.id,
dragState: dragState
? {
isDragging: dragState.isDragging,
draggedComponentId: dragState.draggedComponent?.id,
currentPosition: dragState.currentPosition,
dragStateRef: dragState, // 객체 참조 확인
}
: "null",
});
// 동적 웹타입 목록 가져오기 - API에서 직접 조회
const { webTypes, isLoading: isWebTypesLoading } = useWebTypes({ active: "Y" });
// 강제 리렌더링을 위한 state (드래그 중 실시간 업데이트용)
const [forceRender, setForceRender] = useState(0);
// 드래그 상태를 직접 추적하여 리렌더링 강제
const [lastDragPosition, setLastDragPosition] = useState({ x: 0, y: 0 });
// 웹타입 옵션 생성 - 데이터베이스 기반
const webTypeOptions = webTypes.map((webType) => ({
value: webType.web_type as WebType,
@ -131,6 +157,27 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
const selectedComponentRef = useRef(selectedComponent);
const onUpdatePropertyRef = useRef(onUpdateProperty);
// 실시간 위치 계산 (드래그 중일 때는 dragState.currentPosition 사용)
const getCurrentPosition = () => {
if (dragState?.isDragging && dragState.draggedComponent?.id === selectedComponent?.id) {
console.log("🎯 드래그 중 실시간 위치:", {
draggedId: dragState.draggedComponent?.id,
selectedId: selectedComponent?.id,
currentPosition: dragState.currentPosition,
});
return {
x: Math.round(dragState.currentPosition.x),
y: Math.round(dragState.currentPosition.y),
};
}
return {
x: selectedComponent?.position?.x || 0,
y: selectedComponent?.position?.y || 0,
};
};
const currentPosition = getCurrentPosition();
// 입력 필드들의 로컬 상태 (실시간 타이핑 반영용)
const [localInputs, setLocalInputs] = useState({
placeholder: (selectedComponent?.type === "widget" ? (selectedComponent as WidgetComponent).placeholder : "") || "",
@ -141,8 +188,8 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
? (selectedComponent as AreaComponent).title
: "") || "",
description: (selectedComponent?.type === "area" ? (selectedComponent as AreaComponent).description : "") || "",
positionX: selectedComponent?.position.x?.toString() || "0",
positionY: selectedComponent?.position.y?.toString() || "0",
positionX: currentPosition.x.toString(),
positionY: currentPosition.y.toString(),
positionZ: selectedComponent?.position.z?.toString() || "1",
width: selectedComponent?.size.width?.toString() || "0",
height: selectedComponent?.size.height?.toString() || "0",
@ -174,40 +221,87 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
console.log("🔄 PropertiesPanel: 컴포넌트 변경 감지", {
componentId: selectedComponent.id,
componentType: selectedComponent.type,
isDragging: dragState?.isDragging,
justFinishedDrag: dragState?.justFinishedDrag,
currentValues: {
placeholder: widget?.placeholder,
title: group?.title || area?.title,
description: area?.description,
positionX: selectedComponent.position.x,
labelText: selectedComponent.style?.labelText || selectedComponent.label,
actualPositionX: selectedComponent.position.x,
actualPositionY: selectedComponent.position.y,
dragPositionX: dragState?.currentPosition.x,
dragPositionY: dragState?.currentPosition.y,
},
getCurrentPosResult: getCurrentPosition(),
});
setLocalInputs({
placeholder: widget?.placeholder || "",
title: group?.title || area?.title || "",
description: area?.description || "",
positionX: selectedComponent.position.x?.toString() || "0",
positionY: selectedComponent.position.y?.toString() || "0",
positionZ: selectedComponent.position.z?.toString() || "1",
width: selectedComponent.size.width?.toString() || "0",
height: selectedComponent.size.height?.toString() || "0",
gridColumns: selectedComponent.gridColumns?.toString() || "1",
labelText: selectedComponent.style?.labelText || selectedComponent.label || "",
labelFontSize: selectedComponent.style?.labelFontSize || "12px",
labelColor: selectedComponent.style?.labelColor || "#374151",
labelMarginBottom: selectedComponent.style?.labelMarginBottom || "4px",
required: widget?.required || false,
readonly: widget?.readonly || false,
labelDisplay: selectedComponent.style?.labelDisplay !== false,
// widgetType 동기화
widgetType: widget?.widgetType || "text",
});
// 드래그 중이 아닐 때만 localInputs 업데이트 (드래그 완료 후 최종 위치 반영)
if (!dragState?.isDragging || dragState.draggedComponent?.id !== selectedComponent.id) {
const currentPos = getCurrentPosition();
setLocalInputs({
placeholder: widget?.placeholder || "",
title: group?.title || area?.title || "",
description: area?.description || "",
positionX: currentPos.x.toString(),
positionY: currentPos.y.toString(),
positionZ: selectedComponent.position.z?.toString() || "1",
width: selectedComponent.size.width?.toString() || "0",
height: selectedComponent.size.height?.toString() || "0",
gridColumns: selectedComponent.gridColumns?.toString() || "1",
labelText: selectedComponent.style?.labelText || selectedComponent.label || "",
labelFontSize: selectedComponent.style?.labelFontSize || "12px",
labelColor: selectedComponent.style?.labelColor || "#374151",
labelMarginBottom: selectedComponent.style?.labelMarginBottom || "4px",
required: widget?.required || false,
readonly: widget?.readonly || false,
labelDisplay: selectedComponent.style?.labelDisplay !== false,
// widgetType 동기화
widgetType: widget?.widgetType || "text",
});
console.log("✅ localInputs 업데이트 완료:", {
positionX: currentPos.x.toString(),
positionY: currentPos.y.toString(),
});
}
}
}, [
selectedComponent?.id, // ID만 감지하여 컴포넌트 변경 시에만 업데이트
selectedComponent?.position.x, // 컴포넌트 실제 위치 변경 감지 (드래그 완료 후)
selectedComponent?.position.y,
selectedComponent?.position.z, // z 위치도 감지
dragState?.isDragging, // 드래그 상태 변경 감지 (드래그 완료 감지용)
dragState?.justFinishedDrag, // 드래그 완료 직후 감지
]);
// 렌더링 시마다 실행되는 직접적인 드래그 상태 체크
if (dragState?.isDragging && dragState.draggedComponent?.id === selectedComponent?.id) {
console.log("🎯 렌더링 중 드래그 상태 감지:", {
isDragging: dragState.isDragging,
draggedId: dragState.draggedComponent?.id,
selectedId: selectedComponent?.id,
currentPosition: dragState.currentPosition,
});
const newPosition = {
x: dragState.currentPosition.x,
y: dragState.currentPosition.y,
};
// 위치가 변경되었는지 확인
if (lastDragPosition.x !== newPosition.x || lastDragPosition.y !== newPosition.y) {
console.log("🔄 위치 변경 감지됨:", {
oldPosition: lastDragPosition,
newPosition: newPosition,
});
// 다음 렌더링 사이클에서 업데이트
setTimeout(() => {
setLastDragPosition(newPosition);
setForceRender((prev) => prev + 1);
}, 0);
}
}
if (!selectedComponent) {
return (
<div className="flex h-full flex-col items-center justify-center p-6 text-center">
@ -423,13 +517,26 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
<Input
id="positionX"
type="number"
value={localInputs.positionX}
value={(() => {
const isDragging = dragState?.isDragging && dragState.draggedComponent?.id === selectedComponent?.id;
if (isDragging) {
const realTimeX = Math.round(dragState.currentPosition.x);
console.log("🔥 실시간 X 렌더링:", realTimeX, "forceRender:", forceRender);
return realTimeX.toString();
}
return localInputs.positionX;
})()}
onChange={(e) => {
const newValue = e.target.value;
setLocalInputs((prev) => ({ ...prev, positionX: newValue }));
onUpdateProperty("position", { ...selectedComponent.position, x: Number(newValue) });
}}
className="mt-1"
className={`mt-1 ${
dragState?.isDragging && dragState.draggedComponent?.id === selectedComponent?.id
? "border-blue-300 bg-blue-50 text-blue-700"
: ""
}`}
readOnly={dragState?.isDragging && dragState.draggedComponent?.id === selectedComponent?.id}
/>
</div>
@ -440,13 +547,26 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
<Input
id="positionY"
type="number"
value={localInputs.positionY}
value={(() => {
const isDragging = dragState?.isDragging && dragState.draggedComponent?.id === selectedComponent?.id;
if (isDragging) {
const realTimeY = Math.round(dragState.currentPosition.y);
console.log("🔥 실시간 Y 렌더링:", realTimeY, "forceRender:", forceRender);
return realTimeY.toString();
}
return localInputs.positionY;
})()}
onChange={(e) => {
const newValue = e.target.value;
setLocalInputs((prev) => ({ ...prev, positionY: newValue }));
onUpdateProperty("position", { ...selectedComponent.position, y: Number(newValue) });
}}
className="mt-1"
className={`mt-1 ${
dragState?.isDragging && dragState.draggedComponent?.id === selectedComponent?.id
? "border-blue-300 bg-blue-50 text-blue-700"
: ""
}`}
readOnly={dragState?.isDragging && dragState.draggedComponent?.id === selectedComponent?.id}
/>
</div>

View File

@ -214,8 +214,8 @@ export const DataTableTemplate: React.FC<DataTableTemplateProps> = ({
{filters.length > 0 && (
<div className="flex items-center space-x-2">
<Filter className="text-muted-foreground h-4 w-4" />
{filters.slice(0, 3).map((filter) => (
<Select key={filter.id} disabled={isPreview}>
{filters.slice(0, 3).map((filter, index) => (
<Select key={filter.id || `filter-${index}`} disabled={isPreview}>
<SelectTrigger className="w-[140px]">
<SelectValue placeholder={filter.label} />
</SelectTrigger>