컴포넌트 너비 설정
This commit is contained in:
parent
55f52ed1b5
commit
8bc8df4eb8
|
|
@ -178,8 +178,8 @@ export default function ScreenViewPage() {
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
left: `${component.position.x}px`,
|
left: `${component.position.x}px`,
|
||||||
top: `${component.position.y}px`,
|
top: `${component.position.y}px`,
|
||||||
width: `${component.size.width}px`,
|
width: component.style?.width || `${component.size.width}px`,
|
||||||
height: `${component.size.height}px`,
|
height: component.style?.height || `${component.size.height}px`,
|
||||||
zIndex: component.position.z || 1,
|
zIndex: component.position.z || 1,
|
||||||
backgroundColor: (component as any).backgroundColor || "rgba(59, 130, 246, 0.05)",
|
backgroundColor: (component as any).backgroundColor || "rgba(59, 130, 246, 0.05)",
|
||||||
border: (component as any).border || "1px solid rgba(59, 130, 246, 0.2)",
|
border: (component as any).border || "1px solid rgba(59, 130, 246, 0.2)",
|
||||||
|
|
@ -203,8 +203,8 @@ export default function ScreenViewPage() {
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
left: `${child.position.x}px`,
|
left: `${child.position.x}px`,
|
||||||
top: `${child.position.y}px`,
|
top: `${child.position.y}px`,
|
||||||
width: `${child.size.width}px`,
|
width: child.style?.width || `${child.size.width}px`,
|
||||||
height: `${child.size.height}px`,
|
height: child.style?.height || `${child.size.height}px`,
|
||||||
zIndex: child.position.z || 1,
|
zIndex: child.position.z || 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -277,8 +277,8 @@ export default function ScreenViewPage() {
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
left: `${component.position.x}px`,
|
left: `${component.position.x}px`,
|
||||||
top: `${component.position.y}px`,
|
top: `${component.position.y}px`,
|
||||||
width: `${component.size.width}px`,
|
width: component.style?.width || `${component.size.width}px`,
|
||||||
height: `${component.size.height}px`,
|
height: component.style?.height || `${component.size.height}px`,
|
||||||
zIndex: component.position.z || 1,
|
zIndex: component.position.z || 1,
|
||||||
}}
|
}}
|
||||||
onMouseEnter={() => {
|
onMouseEnter={() => {
|
||||||
|
|
|
||||||
|
|
@ -1698,8 +1698,8 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
left: `${child.position.x - component.position.x}px`,
|
left: `${child.position.x - component.position.x}px`,
|
||||||
top: `${child.position.y - component.position.y}px`,
|
top: `${child.position.y - component.position.y}px`,
|
||||||
width: `${child.size.width}px`,
|
width: child.style?.width || `${child.size.width}px`,
|
||||||
height: `${child.size.height}px`,
|
height: child.style?.height || `${child.size.height}px`,
|
||||||
zIndex: child.position.z || 1,
|
zIndex: child.position.z || 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -1828,8 +1828,8 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||||
style={{
|
style={{
|
||||||
left: `${popupComponent.position.x}px`,
|
left: `${popupComponent.position.x}px`,
|
||||||
top: `${popupComponent.position.y}px`,
|
top: `${popupComponent.position.y}px`,
|
||||||
width: `${popupComponent.size.width}px`,
|
width: popupComponent.style?.width || `${popupComponent.size.width}px`,
|
||||||
height: `${popupComponent.size.height}px`,
|
height: popupComponent.style?.height || `${popupComponent.size.height}px`,
|
||||||
zIndex: Math.min(popupComponent.position.z || 1, 20), // 최대 z-index 20으로 제한
|
zIndex: Math.min(popupComponent.position.z || 1, 20), // 최대 z-index 20으로 제한
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -90,17 +90,43 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||||
: {};
|
: {};
|
||||||
|
|
||||||
// 컴포넌트 기본 스타일 - 레이아웃은 항상 맨 아래
|
// 컴포넌트 기본 스타일 - 레이아웃은 항상 맨 아래
|
||||||
|
// 너비 우선순위: style.width > size.width (픽셀값)
|
||||||
|
const getWidth = () => {
|
||||||
|
// 1순위: style.width가 있으면 우선 사용
|
||||||
|
if (componentStyle?.width) {
|
||||||
|
return componentStyle.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2순위: size.width (픽셀)
|
||||||
|
if (component.componentConfig?.type === "table-list") {
|
||||||
|
return `${Math.max(size?.width || 120, 120)}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${size?.width || 100}px`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getHeight = () => {
|
||||||
|
// 1순위: style.height가 있으면 우선 사용
|
||||||
|
if (componentStyle?.height) {
|
||||||
|
return componentStyle.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2순위: size.height (픽셀)
|
||||||
|
if (component.componentConfig?.type === "table-list") {
|
||||||
|
return `${Math.max(size?.height || 200, 200)}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${size?.height || 40}px`;
|
||||||
|
};
|
||||||
|
|
||||||
const baseStyle = {
|
const baseStyle = {
|
||||||
left: `${position.x}px`,
|
left: `${position.x}px`,
|
||||||
top: `${position.y}px`,
|
top: `${position.y}px`,
|
||||||
width: component.componentConfig?.type === "table-list"
|
width: getWidth(),
|
||||||
? `${Math.max(size?.width || 120, 120)}px` // table-list 디폴트를 그리드 1컬럼 크기로 축소 (120px)
|
height: getHeight(),
|
||||||
: `${size?.width || 100}px`,
|
|
||||||
height: component.componentConfig?.type === "table-list"
|
|
||||||
? `${Math.max(size?.height || 200, 200)}px` // table-list 디폴트 높이도 축소 (200px)
|
|
||||||
: `${size?.height || 36}px`,
|
|
||||||
zIndex: component.type === "layout" ? 1 : position.z || 2, // 레이아웃은 z-index 1, 다른 컴포넌트는 2 이상
|
zIndex: component.type === "layout" ? 1 : position.z || 2, // 레이아웃은 z-index 1, 다른 컴포넌트는 2 이상
|
||||||
...componentStyle,
|
...componentStyle,
|
||||||
|
// style.width와 style.height는 이미 getWidth/getHeight에서 처리했으므로 중복 적용됨
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClick = (e: React.MouseEvent) => {
|
const handleClick = (e: React.MouseEvent) => {
|
||||||
|
|
@ -134,9 +160,7 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||||
onDragEnd={handleDragEnd}
|
onDragEnd={handleDragEnd}
|
||||||
>
|
>
|
||||||
{/* 동적 컴포넌트 렌더링 */}
|
{/* 동적 컴포넌트 렌더링 */}
|
||||||
<div className={`h-full w-full ${
|
<div className={`h-full w-full ${component.componentConfig?.type === "table-list" ? "overflow-hidden" : ""}`}>
|
||||||
component.componentConfig?.type === "table-list" ? "overflow-hidden" : ""
|
|
||||||
}`}>
|
|
||||||
<DynamicComponentRenderer
|
<DynamicComponentRenderer
|
||||||
component={component}
|
component={component}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
|
|
@ -155,7 +179,7 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||||
|
|
||||||
{/* 선택된 컴포넌트 정보 표시 */}
|
{/* 선택된 컴포넌트 정보 표시 */}
|
||||||
{isSelected && (
|
{isSelected && (
|
||||||
<div className="absolute -top-8 left-0 rounded-lg bg-gray-800/90 px-3 py-2 text-xs text-white backdrop-blur-sm shadow-lg">
|
<div className="absolute -top-8 left-0 rounded-lg bg-gray-800/90 px-3 py-2 text-xs text-white shadow-lg backdrop-blur-sm">
|
||||||
{type === "widget" && (
|
{type === "widget" && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{getWidgetIcon((component as WidgetComponent).widgetType)}
|
{getWidgetIcon((component as WidgetComponent).widgetType)}
|
||||||
|
|
|
||||||
|
|
@ -223,6 +223,59 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
||||||
(selectedComponent?.type === "widget" ? (selectedComponent as WidgetComponent).widgetType : "text") || "text",
|
(selectedComponent?.type === "widget" ? (selectedComponent as WidgetComponent).widgetType : "text") || "text",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 너비 드롭다운 로컬 상태 - 실시간 업데이트를 위한 별도 관리
|
||||||
|
const calculateWidthSpan = (width: string | number | undefined): string => {
|
||||||
|
if (!width) return "half";
|
||||||
|
|
||||||
|
if (typeof width === "string" && width.includes("%")) {
|
||||||
|
const percent = parseFloat(width);
|
||||||
|
|
||||||
|
// 정확한 매핑을 위해 가장 가까운 값 찾기
|
||||||
|
// 중복 제거: small(작게) 사용, third(1/3) 사용, twoThirds(2/3) 사용, quarter(1/4) 사용, threeQuarters(3/4) 사용
|
||||||
|
const percentToSpan: Record<number, string> = {
|
||||||
|
100: "full", // 12/12
|
||||||
|
91.666667: "eleven-twelfths", // 11/12
|
||||||
|
83.333333: "five-sixths", // 10/12
|
||||||
|
75: "threeQuarters", // 9/12
|
||||||
|
66.666667: "twoThirds", // 8/12
|
||||||
|
58.333333: "seven-twelfths", // 7/12
|
||||||
|
50: "half", // 6/12
|
||||||
|
41.666667: "five-twelfths", // 5/12
|
||||||
|
33.333333: "third", // 4/12
|
||||||
|
25: "quarter", // 3/12
|
||||||
|
16.666667: "small", // 2/12
|
||||||
|
8.333333: "twelfth", // 1/12
|
||||||
|
};
|
||||||
|
|
||||||
|
// 가장 가까운 퍼센트 값 찾기 (오차 범위 ±2% 허용)
|
||||||
|
let closestSpan = "half";
|
||||||
|
let minDiff = Infinity;
|
||||||
|
|
||||||
|
for (const [key, span] of Object.entries(percentToSpan)) {
|
||||||
|
const diff = Math.abs(percent - parseFloat(key));
|
||||||
|
if (diff < minDiff && diff < 5) {
|
||||||
|
// 5% 오차 범위 내
|
||||||
|
minDiff = diff;
|
||||||
|
closestSpan = span;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return closestSpan;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "half";
|
||||||
|
};
|
||||||
|
|
||||||
|
const [localWidthSpan, setLocalWidthSpan] = useState<string>(() =>
|
||||||
|
calculateWidthSpan(selectedComponent?.style?.width),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 컴포넌트 또는 style.width가 변경될 때 로컬 상태 업데이트
|
||||||
|
useEffect(() => {
|
||||||
|
const newSpan = calculateWidthSpan(selectedComponent?.style?.width);
|
||||||
|
setLocalWidthSpan(newSpan);
|
||||||
|
}, [selectedComponent?.id, selectedComponent?.style?.width]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
selectedComponentRef.current = selectedComponent;
|
selectedComponentRef.current = selectedComponent;
|
||||||
onUpdatePropertyRef.current = onUpdateProperty;
|
onUpdatePropertyRef.current = onUpdateProperty;
|
||||||
|
|
@ -676,13 +729,45 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
||||||
{/* 카드 레이아웃은 자동 크기 계산으로 너비/높이 설정 숨김 */}
|
{/* 카드 레이아웃은 자동 크기 계산으로 너비/높이 설정 숨김 */}
|
||||||
{selectedComponent?.type !== "layout" || (selectedComponent as any)?.layoutType !== "card" ? (
|
{selectedComponent?.type !== "layout" || (selectedComponent as any)?.layoutType !== "card" ? (
|
||||||
<>
|
<>
|
||||||
{/* 🆕 컬럼 스팬 선택 (width 대체) */}
|
{/* 🆕 컬럼 스팬 선택 (width를 퍼센트로 변환) - 기존 UI 유지 */}
|
||||||
<div className="col-span-2">
|
<div className="col-span-2">
|
||||||
<Label className="text-sm font-medium">컴포넌트 너비</Label>
|
<Label className="text-sm font-medium">컴포넌트 너비</Label>
|
||||||
<Select
|
<Select
|
||||||
value={selectedComponent.gridColumnSpan || "half"}
|
value={localWidthSpan}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
onUpdateProperty("gridColumnSpan", value as ColumnSpanPreset);
|
// 컬럼 스팬을 퍼센트로 변환
|
||||||
|
const percentages: Record<string, string> = {
|
||||||
|
// 표준 옵션 (드롭다운에 표시됨)
|
||||||
|
twelfth: "8.333333%", // 1/12
|
||||||
|
small: "16.666667%", // 2/12 (작게)
|
||||||
|
quarter: "25%", // 3/12 (1/4)
|
||||||
|
third: "33.333333%", // 4/12 (1/3)
|
||||||
|
"five-twelfths": "41.666667%", // 5/12
|
||||||
|
half: "50%", // 6/12 (절반)
|
||||||
|
"seven-twelfths": "58.333333%", // 7/12
|
||||||
|
twoThirds: "66.666667%", // 8/12 (2/3)
|
||||||
|
threeQuarters: "75%", // 9/12 (3/4)
|
||||||
|
"five-sixths": "83.333333%", // 10/12
|
||||||
|
"eleven-twelfths": "91.666667%", // 11/12
|
||||||
|
full: "100%", // 12/12 (전체)
|
||||||
|
|
||||||
|
// 레거시 호환성 (드롭다운에는 없지만 기존 데이터 지원)
|
||||||
|
sixth: "16.666667%", // 2/12 (= small)
|
||||||
|
label: "25%", // 3/12 (= quarter)
|
||||||
|
medium: "33.333333%", // 4/12 (= third)
|
||||||
|
large: "66.666667%", // 8/12 (= twoThirds)
|
||||||
|
input: "75%", // 9/12 (= threeQuarters)
|
||||||
|
"two-thirds": "66.666667%", // 케밥케이스 호환
|
||||||
|
"three-quarters": "75%", // 케밥케이스 호환
|
||||||
|
};
|
||||||
|
|
||||||
|
const newWidth = percentages[value] || "50%";
|
||||||
|
|
||||||
|
// 로컬 상태 즉시 업데이트
|
||||||
|
setLocalWidthSpan(value);
|
||||||
|
|
||||||
|
// 컴포넌트 속성 업데이트
|
||||||
|
onUpdateProperty("style.width", newWidth);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="mt-1">
|
<SelectTrigger className="mt-1">
|
||||||
|
|
@ -690,7 +775,12 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{Object.entries(COLUMN_SPAN_PRESETS)
|
{Object.entries(COLUMN_SPAN_PRESETS)
|
||||||
.filter(([key]) => key !== "auto")
|
.filter(([key]) => {
|
||||||
|
// auto 제거 및 중복 퍼센트 옵션 제거
|
||||||
|
// 제거할 옵션: auto, label(=quarter), input(=threeQuarters), medium(=third), large(=twoThirds)
|
||||||
|
const excludeKeys = ["auto", "label", "input", "medium", "large"];
|
||||||
|
return !excludeKeys.includes(key);
|
||||||
|
})
|
||||||
.map(([key, info]) => (
|
.map(([key, info]) => (
|
||||||
<SelectItem key={key} value={key}>
|
<SelectItem key={key} value={key}>
|
||||||
<div className="flex w-full items-center justify-between gap-4">
|
<div className="flex w-full items-center justify-between gap-4">
|
||||||
|
|
@ -704,14 +794,39 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
{/* 시각적 프리뷰 */}
|
{/* 시각적 프리뷰 - 기존 UI 유지, localWidthSpan 기반 */}
|
||||||
<div className="mt-3 space-y-2">
|
<div className="mt-3 space-y-2">
|
||||||
<Label className="text-xs text-gray-500">미리보기</Label>
|
<Label className="text-xs text-gray-500">미리보기</Label>
|
||||||
<div className="grid h-6 grid-cols-12 gap-0.5 overflow-hidden rounded border">
|
<div className="grid h-6 grid-cols-12 gap-0.5 overflow-hidden rounded border">
|
||||||
{Array.from({ length: 12 }).map((_, i) => {
|
{Array.from({ length: 12 }).map((_, i) => {
|
||||||
const spanValue = COLUMN_SPAN_VALUES[selectedComponent.gridColumnSpan || "half"];
|
// localWidthSpan으로부터 활성 컬럼 계산
|
||||||
const startCol = selectedComponent.gridColumnStart || 1;
|
const spanValues: Record<string, number> = {
|
||||||
const isActive = i + 1 >= startCol && i + 1 < startCol + spanValue;
|
// 표준 옵션
|
||||||
|
twelfth: 1,
|
||||||
|
small: 2,
|
||||||
|
quarter: 3,
|
||||||
|
third: 4,
|
||||||
|
"five-twelfths": 5,
|
||||||
|
half: 6,
|
||||||
|
"seven-twelfths": 7,
|
||||||
|
twoThirds: 8,
|
||||||
|
threeQuarters: 9,
|
||||||
|
"five-sixths": 10,
|
||||||
|
"eleven-twelfths": 11,
|
||||||
|
full: 12,
|
||||||
|
|
||||||
|
// 레거시 호환성
|
||||||
|
sixth: 2,
|
||||||
|
label: 3,
|
||||||
|
medium: 4,
|
||||||
|
large: 8,
|
||||||
|
input: 9,
|
||||||
|
"two-thirds": 8,
|
||||||
|
"three-quarters": 9,
|
||||||
|
};
|
||||||
|
|
||||||
|
const spanValue = spanValues[localWidthSpan] || 6;
|
||||||
|
const isActive = i < spanValue;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -722,43 +837,36 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-center text-xs text-gray-500">
|
<p className="text-center text-xs text-gray-500">
|
||||||
{COLUMN_SPAN_VALUES[selectedComponent.gridColumnSpan || "half"]} / 12 컬럼
|
{(() => {
|
||||||
|
const spanValues: Record<string, number> = {
|
||||||
|
// 표준 옵션
|
||||||
|
twelfth: 1,
|
||||||
|
small: 2,
|
||||||
|
quarter: 3,
|
||||||
|
third: 4,
|
||||||
|
"five-twelfths": 5,
|
||||||
|
half: 6,
|
||||||
|
"seven-twelfths": 7,
|
||||||
|
twoThirds: 8,
|
||||||
|
threeQuarters: 9,
|
||||||
|
"five-sixths": 10,
|
||||||
|
"eleven-twelfths": 11,
|
||||||
|
full: 12,
|
||||||
|
|
||||||
|
// 레거시 호환성
|
||||||
|
sixth: 2,
|
||||||
|
label: 3,
|
||||||
|
medium: 4,
|
||||||
|
large: 8,
|
||||||
|
input: 9,
|
||||||
|
"two-thirds": 8,
|
||||||
|
"three-quarters": 9,
|
||||||
|
};
|
||||||
|
const cols = spanValues[localWidthSpan] || 6;
|
||||||
|
return `${cols} / 12 컬럼`;
|
||||||
|
})()}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 고급 설정 */}
|
|
||||||
<Collapsible className="mt-3">
|
|
||||||
<CollapsibleTrigger asChild>
|
|
||||||
<Button variant="ghost" size="sm" className="w-full justify-between">
|
|
||||||
<span className="text-xs">고급 설정</span>
|
|
||||||
<ChevronDown className="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
<CollapsibleContent className="mt-2 space-y-2">
|
|
||||||
<div>
|
|
||||||
<Label className="text-xs">시작 컬럼 위치</Label>
|
|
||||||
<Select
|
|
||||||
value={selectedComponent.gridColumnStart?.toString() || "auto"}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
onUpdateProperty("gridColumnStart", value === "auto" ? undefined : parseInt(value));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="mt-1">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="auto">자동</SelectItem>
|
|
||||||
{Array.from({ length: 12 }, (_, i) => (
|
|
||||||
<SelectItem key={i + 1} value={(i + 1).toString()}>
|
|
||||||
{i + 1}번 컬럼부터
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<p className="mt-1 text-xs text-gray-500">"자동"을 선택하면 이전 컴포넌트 다음에 배치됩니다</p>
|
|
||||||
</div>
|
|
||||||
</CollapsibleContent>
|
|
||||||
</Collapsible>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue