컴포넌트 너비 설정

This commit is contained in:
kjs 2025-10-14 13:27:02 +09:00
parent 55f52ed1b5
commit 8bc8df4eb8
4 changed files with 195 additions and 63 deletions

View File

@ -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={() => {

View File

@ -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으로 제한
}} }}
> >

View File

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

View File

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