fix: tabs-component 충돌 해결 - origin/main 버전으로 복원

배지 기능(HEAD)을 제거하고 origin/main 버전을 유지함.
충돌 시 작성하지 않은 파일은 main 버전을 그대로 사용.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
shin 2026-03-10 18:36:27 +09:00
parent afeca4fa00
commit 87b8aec759
1 changed files with 116 additions and 81 deletions

View File

@ -21,19 +21,21 @@ const TabsDesignEditor: React.FC<{
const [dragPosition, setDragPosition] = useState<{ x: number; y: number } | null>(null);
const containerRef = useRef<HTMLDivElement>(null);
const rafRef = useRef<number | null>(null);
// 리사이즈 상태
const [resizingCompId, setResizingCompId] = useState<string | null>(null);
const [resizeSize, setResizeSize] = useState<{ width: number; height: number } | null>(null);
const [lastResizedCompId, setLastResizedCompId] = useState<string | null>(null);
const activeTab = tabs.find((t) => t.id === activeTabId);
// 🆕 탭 컴포넌트 size가 업데이트되면 resizeSize 초기화
useEffect(() => {
if (resizeSize && lastResizedCompId && !resizingCompId) {
const targetComp = activeTab?.components?.find((c) => c.id === lastResizedCompId);
if (targetComp && targetComp.size?.width === resizeSize.width && targetComp.size?.height === resizeSize.height) {
const targetComp = activeTab?.components?.find(c => c.id === lastResizedCompId);
if (targetComp &&
targetComp.size?.width === resizeSize.width &&
targetComp.size?.height === resizeSize.height) {
setResizeSize(null);
setLastResizedCompId(null);
}
@ -46,7 +48,7 @@ const TabsDesignEditor: React.FC<{
"px-4 py-2 text-sm font-medium cursor-pointer transition-colors",
isActive
? "bg-background border-b-2 border-primary text-primary"
: "text-muted-foreground hover:text-foreground hover:bg-muted/50",
: "text-muted-foreground hover:text-foreground hover:bg-muted/50"
);
};
@ -54,7 +56,7 @@ const TabsDesignEditor: React.FC<{
const handleDeleteComponent = useCallback(
(compId: string) => {
if (!onUpdateComponent) return;
const updatedTabs = tabs.map((tab) => {
if (tab.id === activeTabId) {
return {
@ -73,7 +75,7 @@ const TabsDesignEditor: React.FC<{
},
});
},
[activeTabId, component, onUpdateComponent, tabs],
[activeTabId, component, onUpdateComponent, tabs]
);
// 10px 단위 스냅 함수
@ -84,13 +86,13 @@ const TabsDesignEditor: React.FC<{
(e: React.MouseEvent, comp: TabInlineComponent) => {
e.stopPropagation();
e.preventDefault();
// 드래그 시작 시 마우스 위치와 컴포넌트의 현재 위치 저장
const startMouseX = e.clientX;
const startMouseY = e.clientY;
const startLeft = comp.position?.x || 0;
const startTop = comp.position?.y || 0;
setDraggingCompId(comp.id);
setDragPosition({ x: startLeft, y: startTop });
@ -99,12 +101,12 @@ const TabsDesignEditor: React.FC<{
if (rafRef.current) {
cancelAnimationFrame(rafRef.current);
}
rafRef.current = requestAnimationFrame(() => {
// 마우스 이동량 계산
const deltaX = moveEvent.clientX - startMouseX;
const deltaY = moveEvent.clientY - startMouseY;
// 새 위치 = 시작 위치 + 이동량 (10px 단위 스냅 적용)
const newX = snapTo10(Math.max(0, startLeft + deltaX));
const newY = snapTo10(Math.max(0, startTop + deltaY));
@ -117,20 +119,20 @@ const TabsDesignEditor: React.FC<{
const handleMouseUp = (upEvent: MouseEvent) => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
if (rafRef.current) {
cancelAnimationFrame(rafRef.current);
rafRef.current = null;
}
// 마우스 이동량 계산
const deltaX = upEvent.clientX - startMouseX;
const deltaY = upEvent.clientY - startMouseY;
// 새 위치 = 시작 위치 + 이동량 (10px 단위 스냅 적용)
const newX = snapTo10(Math.max(0, startLeft + deltaX));
const newY = snapTo10(Math.max(0, startTop + deltaY));
setDraggingCompId(null);
setDragPosition(null);
@ -149,7 +151,7 @@ const TabsDesignEditor: React.FC<{
y: newY,
},
}
: c,
: c
),
};
}
@ -173,7 +175,7 @@ const TabsDesignEditor: React.FC<{
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
},
[activeTabId, component, onUpdateComponent, tabs, snapTo10],
[activeTabId, component, onUpdateComponent, tabs, snapTo10]
);
// 리사이즈 시작 핸들러
@ -181,12 +183,12 @@ const TabsDesignEditor: React.FC<{
(e: React.MouseEvent, comp: TabInlineComponent, direction: "e" | "s" | "se") => {
e.stopPropagation();
e.preventDefault();
const startMouseX = e.clientX;
const startMouseY = e.clientY;
const startWidth = comp.size?.width || 200;
const startHeight = comp.size?.height || 100;
setResizingCompId(comp.id);
setResizeSize({ width: startWidth, height: startHeight });
@ -194,21 +196,21 @@ const TabsDesignEditor: React.FC<{
if (rafRef.current) {
cancelAnimationFrame(rafRef.current);
}
rafRef.current = requestAnimationFrame(() => {
const deltaX = moveEvent.clientX - startMouseX;
const deltaY = moveEvent.clientY - startMouseY;
let newWidth = startWidth;
let newHeight = startHeight;
if (direction === "e" || direction === "se") {
newWidth = snapTo10(Math.max(50, startWidth + deltaX));
}
if (direction === "s" || direction === "se") {
newHeight = snapTo10(Math.max(20, startHeight + deltaY));
}
setResizeSize({ width: newWidth, height: newHeight });
});
};
@ -216,18 +218,18 @@ const TabsDesignEditor: React.FC<{
const handleMouseUp = (upEvent: MouseEvent) => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
if (rafRef.current) {
cancelAnimationFrame(rafRef.current);
rafRef.current = null;
}
const deltaX = upEvent.clientX - startMouseX;
const deltaY = upEvent.clientY - startMouseY;
let newWidth = startWidth;
let newHeight = startHeight;
if (direction === "e" || direction === "se") {
newWidth = snapTo10(Math.max(50, startWidth + deltaX));
}
@ -250,7 +252,7 @@ const TabsDesignEditor: React.FC<{
height: newHeight,
},
}
: c,
: c
),
};
}
@ -265,7 +267,7 @@ const TabsDesignEditor: React.FC<{
},
});
}
// 🆕 리사이즈 상태 해제 (resizeSize는 마지막 크기 유지, lastResizedCompId 설정)
setLastResizedCompId(comp.id);
setResizingCompId(null);
@ -274,13 +276,13 @@ const TabsDesignEditor: React.FC<{
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
},
[activeTabId, component, onUpdateComponent, tabs],
[activeTabId, component, onUpdateComponent, tabs]
);
return (
<div className="bg-background flex h-full w-full flex-col overflow-hidden rounded-lg border">
<div className="flex h-full w-full flex-col overflow-hidden rounded-lg border bg-background">
{/* 탭 헤더 */}
<div className="bg-muted/30 flex items-center border-b">
<div className="flex items-center border-b bg-muted/30">
{tabs.length > 0 ? (
tabs.map((tab) => (
<div
@ -293,13 +295,12 @@ const TabsDesignEditor: React.FC<{
}}
>
{tab.label || "탭"}
{tab.components && tab.components.length > 0 && (
<span className="text-muted-foreground ml-1 text-xs">({tab.components.length})</span>
)}
</div>
))
) : (
<div className="text-muted-foreground px-4 py-2 text-sm"> </div>
<div className="px-4 py-2 text-sm text-muted-foreground">
</div>
)}
</div>
@ -312,7 +313,10 @@ const TabsDesignEditor: React.FC<{
onClick={() => onSelectTabComponent?.(activeTabId, "", {} as TabInlineComponent)}
>
{activeTab ? (
<div ref={containerRef} className="absolute inset-0 overflow-auto p-2">
<div
ref={containerRef}
className="absolute inset-0 overflow-auto p-2"
>
{activeTab.components && activeTab.components.length > 0 ? (
<div className="relative" style={{ minHeight: "100%", minWidth: "100%" }}>
{activeTab.components.map((comp: TabInlineComponent) => {
@ -341,8 +345,8 @@ const TabsDesignEditor: React.FC<{
};
// 드래그 중인 컴포넌트는 dragPosition 사용, 아니면 저장된 position 사용
const displayX = isDragging && dragPosition ? dragPosition.x : comp.position?.x || 0;
const displayY = isDragging && dragPosition ? dragPosition.y : comp.position?.y || 0;
const displayX = isDragging && dragPosition ? dragPosition.x : (comp.position?.x || 0);
const displayY = isDragging && dragPosition ? dragPosition.y : (comp.position?.y || 0);
return (
<div
@ -356,11 +360,7 @@ const TabsDesignEditor: React.FC<{
}}
onClick={(e) => {
e.stopPropagation();
console.log("🔍 [탭 컴포넌트] 클릭:", {
activeTabId,
compId: comp.id,
hasOnSelectTabComponent: !!onSelectTabComponent,
});
console.log("🔍 [탭 컴포넌트] 클릭:", { activeTabId, compId: comp.id, hasOnSelectTabComponent: !!onSelectTabComponent });
onSelectTabComponent?.(activeTabId, comp.id, comp);
}}
>
@ -368,14 +368,14 @@ const TabsDesignEditor: React.FC<{
<div
className={cn(
"flex h-4 cursor-move items-center justify-between rounded-t border border-b-0 bg-gray-100 px-1",
isSelected ? "border-primary" : "border-gray-200",
isSelected ? "border-primary" : "border-gray-200"
)}
style={{ width: comp.size?.width || 200 }}
onMouseDown={(e) => handleDragStart(e, comp)}
>
<div className="flex items-center gap-0.5">
<Move className="h-2.5 w-2.5 text-gray-400" />
<span className="max-w-[100px] truncate text-[9px] text-gray-500">
<span className="text-[9px] text-gray-500 truncate max-w-[100px]">
{comp.label || comp.componentType}
</span>
</div>
@ -404,42 +404,44 @@ const TabsDesignEditor: React.FC<{
</div>
{/* 실제 컴포넌트 렌더링 - 핸들 아래에 별도 영역 */}
<div
<div
className={cn(
"relative overflow-hidden rounded-b border bg-white shadow-sm",
isSelected ? "border-primary ring-primary/30 ring-2" : "border-gray-200",
"relative rounded-b border bg-white shadow-sm overflow-hidden",
isSelected
? "border-primary ring-2 ring-primary/30"
: "border-gray-200",
(isDragging || isResizing) && "opacity-80 shadow-lg",
!(isDragging || isResizing) && "transition-all",
!(isDragging || isResizing) && "transition-all"
)}
style={{
width: displayWidth,
height: displayHeight,
}}
>
<div className="pointer-events-none h-full w-full">
<div className="h-full w-full pointer-events-none">
<DynamicComponentRenderer
component={componentData as any}
isDesignMode={true}
formData={{}}
/>
</div>
{/* 리사이즈 가장자리 영역 - 선택된 컴포넌트에만 표시 */}
{isSelected && (
<>
{/* 오른쪽 가장자리 (너비 조절) */}
<div
className="hover:bg-primary/10 pointer-events-auto absolute top-0 right-0 z-10 h-full w-2 cursor-ew-resize"
className="absolute top-0 right-0 w-2 h-full cursor-ew-resize pointer-events-auto z-10 hover:bg-primary/10"
onMouseDown={(e) => handleResizeStart(e, comp, "e")}
/>
{/* 아래 가장자리 (높이 조절) */}
<div
className="hover:bg-primary/10 pointer-events-auto absolute bottom-0 left-0 z-10 h-2 w-full cursor-ns-resize"
className="absolute bottom-0 left-0 w-full h-2 cursor-ns-resize pointer-events-auto z-10 hover:bg-primary/10"
onMouseDown={(e) => handleResizeStart(e, comp, "s")}
/>
{/* 오른쪽 아래 모서리 (너비+높이 조절) */}
<div
className="hover:bg-primary/20 pointer-events-auto absolute right-0 bottom-0 z-20 h-3 w-3 cursor-nwse-resize"
className="absolute bottom-0 right-0 w-3 h-3 cursor-nwse-resize pointer-events-auto z-20 hover:bg-primary/20"
onMouseDown={(e) => handleResizeStart(e, comp, "se")}
/>
</>
@ -452,14 +454,20 @@ const TabsDesignEditor: React.FC<{
) : (
<div className="flex h-full w-full flex-col items-center justify-center rounded border-2 border-dashed border-gray-300 bg-gray-50/50">
<Plus className="mb-2 h-8 w-8 text-gray-400" />
<p className="text-sm font-medium text-gray-500"> </p>
<p className="mt-1 text-xs text-gray-400"> </p>
<p className="text-sm font-medium text-gray-500">
</p>
<p className="mt-1 text-xs text-gray-400">
</p>
</div>
)}
</div>
) : (
<div className="flex h-full w-full items-center justify-center">
<p className="text-muted-foreground text-sm"> </p>
<p className="text-sm text-muted-foreground">
</p>
</div>
)}
</div>
@ -469,13 +477,19 @@ const TabsDesignEditor: React.FC<{
// TabsWidget 래퍼 컴포넌트
const TabsWidgetWrapper: React.FC<any> = (props) => {
const { component, isDesignMode, onUpdateComponent, onSelectTabComponent, selectedTabComponentId, ...restProps } =
props;
const {
component,
isDesignMode,
onUpdateComponent,
onSelectTabComponent,
selectedTabComponentId,
...restProps
} = props;
// componentConfig에서 탭 정보 추출
const tabsConfig = component.componentConfig || {};
const tabs: TabItem[] = tabsConfig.tabs || [];
// 🎯 디자인 모드에서는 드롭 가능한 에디터 UI 렌더링
if (isDesignMode) {
return (
@ -501,7 +515,8 @@ const TabsWidgetWrapper: React.FC<any> = (props) => {
persistSelection: tabsConfig.persistSelection || false,
};
const TabsWidget = require("@/components/screen/widgets/TabsWidget").TabsWidget;
const TabsWidget =
require("@/components/screen/widgets/TabsWidget").TabsWidget;
return (
<div className="h-full w-full">
@ -518,7 +533,8 @@ const TabsWidgetWrapper: React.FC<any> = (props) => {
ComponentRegistry.registerComponent({
id: "v2-tabs-widget",
name: "탭 컴포넌트",
description: "탭별로 컴포넌트를 자유롭게 배치할 수 있는 레이아웃 컴포넌트입니다.",
description:
"탭별로 컴포넌트를 자유롭게 배치할 수 있는 레이아웃 컴포넌트입니다.",
category: ComponentCategory.LAYOUT,
webType: "text" as any,
component: TabsWidgetWrapper,
@ -580,12 +596,20 @@ ComponentRegistry.registerComponent({
},
// 에디터 모드에서의 렌더링 - 탭 선택 및 컴포넌트 드롭 지원
renderEditor: ({ component, isSelected, onClick, onDragStart, onDragEnd }) => {
renderEditor: ({
component,
isSelected,
onClick,
onDragStart,
onDragEnd,
}) => {
const tabsConfig = (component as any).componentConfig || {};
const tabs: TabItem[] = tabsConfig.tabs || [];
// 에디터 모드에서 선택된 탭 상태 관리
const [activeTabId, setActiveTabId] = useState<string>(tabs[0]?.id || "");
const [activeTabId, setActiveTabId] = useState<string>(
tabs[0]?.id || ""
);
const activeTab = tabs.find((t) => t.id === activeTabId);
@ -596,19 +620,19 @@ ComponentRegistry.registerComponent({
"px-4 py-2 text-sm font-medium cursor-pointer transition-colors",
isActive
? "bg-background border-b-2 border-primary text-primary"
: "text-muted-foreground hover:text-foreground hover:bg-muted/50",
: "text-muted-foreground hover:text-foreground hover:bg-muted/50"
);
};
return (
<div
className="bg-background flex h-full w-full flex-col overflow-hidden rounded-lg border"
className="flex h-full w-full flex-col overflow-hidden rounded-lg border bg-background"
onClick={onClick}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
>
{/* 탭 헤더 */}
<div className="bg-muted/30 flex items-center border-b">
<div className="flex items-center border-b bg-muted/30">
{tabs.length > 0 ? (
tabs.map((tab) => (
<div
@ -620,13 +644,12 @@ ComponentRegistry.registerComponent({
}}
>
{tab.label || "탭"}
{tab.components && tab.components.length > 0 && (
<span className="text-muted-foreground ml-1 text-xs">({tab.components.length})</span>
)}
</div>
))
) : (
<div className="text-muted-foreground px-4 py-2 text-sm"> </div>
<div className="px-4 py-2 text-sm text-muted-foreground">
</div>
)}
</div>
@ -653,8 +676,12 @@ ComponentRegistry.registerComponent({
}}
>
<div className="flex h-full flex-col items-center justify-center">
<span className="text-xs font-medium text-gray-600">{comp.label || comp.componentType}</span>
<span className="text-[10px] text-gray-400">{comp.componentType}</span>
<span className="text-xs font-medium text-gray-600">
{comp.label || comp.componentType}
</span>
<span className="text-[10px] text-gray-400">
{comp.componentType}
</span>
</div>
</div>
))}
@ -662,21 +689,27 @@ ComponentRegistry.registerComponent({
) : (
<div className="flex h-full w-full flex-col items-center justify-center rounded border-2 border-dashed border-gray-300 bg-gray-50/50">
<Plus className="mb-2 h-8 w-8 text-gray-400" />
<p className="text-sm font-medium text-gray-500"> </p>
<p className="mt-1 text-xs text-gray-400"> </p>
<p className="text-sm font-medium text-gray-500">
</p>
<p className="mt-1 text-xs text-gray-400">
</p>
</div>
)}
</div>
) : (
<div className="flex h-full w-full items-center justify-center">
<p className="text-muted-foreground text-sm"> </p>
<p className="text-sm text-muted-foreground">
</p>
</div>
)}
</div>
{/* 선택 표시 */}
{isSelected && (
<div className="ring-primary pointer-events-none absolute inset-0 rounded-lg ring-2 ring-offset-2" />
<div className="pointer-events-none absolute inset-0 rounded-lg ring-2 ring-primary ring-offset-2" />
)}
</div>
);
@ -689,9 +722,11 @@ ComponentRegistry.registerComponent({
// 설정 패널
configPanel: React.lazy(() =>
import("@/components/screen/config-panels/TabsConfigPanel").then((module) => ({
default: module.TabsConfigPanel,
})),
import("@/components/screen/config-panels/TabsConfigPanel").then(
(module) => ({
default: module.TabsConfigPanel,
})
)
),
// 검증 함수