1001 lines
26 KiB
Markdown
1001 lines
26 KiB
Markdown
# 🗑️ Width 속성 완전 제거 계획서
|
|
|
|
## 🎯 목표
|
|
|
|
현재 화면 관리 시스템에서 **픽셀 기반 width 설정을 완전히 제거**하고, **컬럼 수(gridColumnSpan)로만 제어**하도록 변경
|
|
|
|
## 📊 현재 Width 사용 현황
|
|
|
|
### 1. 타입 정의에서의 width
|
|
|
|
```typescript
|
|
// frontend/types/screen-management.ts
|
|
export interface BaseComponent {
|
|
size: Size; // ❌ 제거 대상
|
|
}
|
|
|
|
export interface Size {
|
|
width: number; // ❌ 제거
|
|
height: number; // ✅ 유지 (행 높이 제어용)
|
|
}
|
|
```
|
|
|
|
### 2. PropertiesPanel에서 width 입력 UI
|
|
|
|
**위치**: `frontend/components/screen/panels/PropertiesPanel.tsx`
|
|
|
|
- 라인 665-680: 너비 입력 필드
|
|
- 라인 1081-1092: 사이드바 너비 설정
|
|
|
|
### 3. StyleEditor에서 width 스타일
|
|
|
|
**위치**: `frontend/components/screen/ScreenDesigner.tsx`
|
|
|
|
- 라인 3874-3891: 스타일에서 width 추출 및 적용
|
|
|
|
### 4. 컴포넌트 렌더링에서 width 사용
|
|
|
|
**위치**: `frontend/components/screen/layout/ContainerComponent.tsx`
|
|
|
|
- 라인 27: `gridColumn: span ${component.size.width}`
|
|
|
|
### 5. 템플릿에서 width 정의
|
|
|
|
**위치**: `frontend/components/screen/panels/TemplatesPanel.tsx`
|
|
|
|
- 라인 48: `defaultSize: { width, height }`
|
|
- 라인 54: `size: { width, height }`
|
|
|
|
---
|
|
|
|
## 🔄 마이그레이션 전략
|
|
|
|
### Phase 1: 타입 시스템 수정
|
|
|
|
#### 1.1 새로운 타입 정의
|
|
|
|
```typescript
|
|
// frontend/types/screen-management.ts
|
|
|
|
/**
|
|
* 🆕 새로운 Size 인터페이스 (width 제거)
|
|
*/
|
|
export interface Size {
|
|
height: number; // 행 높이만 제어
|
|
}
|
|
|
|
/**
|
|
* 🆕 BaseComponent 확장
|
|
*/
|
|
export interface BaseComponent {
|
|
id: string;
|
|
type: ComponentType;
|
|
position: Position; // y 좌표만 사용 (행 위치)
|
|
size: Size; // height만 포함
|
|
|
|
// 🆕 그리드 시스템 속성
|
|
gridColumnSpan: ColumnSpanPreset; // 필수: 컬럼 너비
|
|
gridColumnStart?: number; // 선택: 시작 컬럼
|
|
gridRowIndex: number; // 필수: 행 인덱스
|
|
|
|
parentId?: string;
|
|
label?: string;
|
|
required?: boolean;
|
|
readonly?: boolean;
|
|
style?: ComponentStyle;
|
|
className?: string;
|
|
}
|
|
|
|
/**
|
|
* 🆕 컬럼 스팬 프리셋
|
|
*/
|
|
export type ColumnSpanPreset =
|
|
| "full" // 12 컬럼
|
|
| "half" // 6 컬럼
|
|
| "third" // 4 컬럼
|
|
| "twoThirds" // 8 컬럼
|
|
| "quarter" // 3 컬럼
|
|
| "threeQuarters" // 9 컬럼
|
|
| "label" // 3 컬럼 (라벨용)
|
|
| "input" // 9 컬럼 (입력용)
|
|
| "small" // 2 컬럼
|
|
| "medium" // 4 컬럼
|
|
| "large" // 8 컬럼
|
|
| "auto"; // 자동 계산
|
|
|
|
export const COLUMN_SPAN_VALUES: Record<ColumnSpanPreset, number> = {
|
|
full: 12,
|
|
half: 6,
|
|
third: 4,
|
|
twoThirds: 8,
|
|
quarter: 3,
|
|
threeQuarters: 9,
|
|
label: 3,
|
|
input: 9,
|
|
small: 2,
|
|
medium: 4,
|
|
large: 8,
|
|
auto: 0, // 자동 계산
|
|
};
|
|
```
|
|
|
|
#### 1.2 ComponentStyle 수정 (width 제거)
|
|
|
|
```typescript
|
|
export interface ComponentStyle extends CommonStyle {
|
|
// ❌ 제거: width
|
|
// ✅ 유지: height (컴포넌트 자체 높이)
|
|
height?: string;
|
|
|
|
// 나머지 스타일 속성들
|
|
margin?: string;
|
|
padding?: string;
|
|
backgroundColor?: string;
|
|
// ... 기타
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Phase 2: UI 컴포넌트 수정
|
|
|
|
#### 2.1 PropertiesPanel 수정
|
|
|
|
**파일**: `frontend/components/screen/panels/PropertiesPanel.tsx`
|
|
|
|
**변경 전**:
|
|
|
|
```typescript
|
|
// 라인 665-680
|
|
<div>
|
|
<Label htmlFor="width" className="text-sm font-medium">
|
|
너비
|
|
</Label>
|
|
<Input
|
|
id="width"
|
|
type="number"
|
|
value={localInputs.width}
|
|
onChange={(e) => {
|
|
const newValue = e.target.value;
|
|
setLocalInputs((prev) => ({ ...prev, width: newValue }));
|
|
onUpdateProperty("size.width", Number(newValue));
|
|
}}
|
|
className="mt-1"
|
|
/>
|
|
</div>
|
|
```
|
|
|
|
**변경 후**:
|
|
|
|
```typescript
|
|
{
|
|
/* 🆕 컬럼 스팬 선택 */
|
|
}
|
|
<div>
|
|
<Label className="text-sm font-medium">컴포넌트 너비</Label>
|
|
<Select
|
|
value={selectedComponent.gridColumnSpan || "half"}
|
|
onValueChange={(value) => {
|
|
onUpdateProperty("gridColumnSpan", value as ColumnSpanPreset);
|
|
}}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="full">
|
|
<div className="flex items-center justify-between w-full">
|
|
<span>전체</span>
|
|
<span className="text-xs text-gray-500 ml-4">12/12 (100%)</span>
|
|
</div>
|
|
</SelectItem>
|
|
<SelectItem value="half">
|
|
<div className="flex items-center justify-between w-full">
|
|
<span>절반</span>
|
|
<span className="text-xs text-gray-500 ml-4">6/12 (50%)</span>
|
|
</div>
|
|
</SelectItem>
|
|
<SelectItem value="third">
|
|
<div className="flex items-center justify-between w-full">
|
|
<span>1/3</span>
|
|
<span className="text-xs text-gray-500 ml-4">4/12 (33%)</span>
|
|
</div>
|
|
</SelectItem>
|
|
<SelectItem value="twoThirds">
|
|
<div className="flex items-center justify-between w-full">
|
|
<span>2/3</span>
|
|
<span className="text-xs text-gray-500 ml-4">8/12 (67%)</span>
|
|
</div>
|
|
</SelectItem>
|
|
<SelectItem value="quarter">
|
|
<div className="flex items-center justify-between w-full">
|
|
<span>1/4</span>
|
|
<span className="text-xs text-gray-500 ml-4">3/12 (25%)</span>
|
|
</div>
|
|
</SelectItem>
|
|
<SelectItem value="threeQuarters">
|
|
<div className="flex items-center justify-between w-full">
|
|
<span>3/4</span>
|
|
<span className="text-xs text-gray-500 ml-4">9/12 (75%)</span>
|
|
</div>
|
|
</SelectItem>
|
|
<SelectSeparator />
|
|
<SelectItem value="label">
|
|
<div className="flex items-center justify-between w-full">
|
|
<span>라벨용</span>
|
|
<span className="text-xs text-gray-500 ml-4">3/12 (25%)</span>
|
|
</div>
|
|
</SelectItem>
|
|
<SelectItem value="input">
|
|
<div className="flex items-center justify-between w-full">
|
|
<span>입력용</span>
|
|
<span className="text-xs text-gray-500 ml-4">9/12 (75%)</span>
|
|
</div>
|
|
</SelectItem>
|
|
<SelectItem value="small">
|
|
<div className="flex items-center justify-between w-full">
|
|
<span>작게</span>
|
|
<span className="text-xs text-gray-500 ml-4">2/12 (17%)</span>
|
|
</div>
|
|
</SelectItem>
|
|
<SelectItem value="medium">
|
|
<div className="flex items-center justify-between w-full">
|
|
<span>보통</span>
|
|
<span className="text-xs text-gray-500 ml-4">4/12 (33%)</span>
|
|
</div>
|
|
</SelectItem>
|
|
<SelectItem value="large">
|
|
<div className="flex items-center justify-between w-full">
|
|
<span>크게</span>
|
|
<span className="text-xs text-gray-500 ml-4">8/12 (67%)</span>
|
|
</div>
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
{/* 시각적 프리뷰 */}
|
|
<div className="mt-3 space-y-2">
|
|
<Label className="text-xs text-gray-500">미리보기</Label>
|
|
<div className="grid grid-cols-12 gap-0.5 h-6 rounded border overflow-hidden">
|
|
{Array.from({ length: 12 }).map((_, i) => {
|
|
const spanValue =
|
|
COLUMN_SPAN_VALUES[selectedComponent.gridColumnSpan || "half"];
|
|
const startCol = selectedComponent.gridColumnStart || 1;
|
|
const isActive = i + 1 >= startCol && i + 1 < startCol + spanValue;
|
|
|
|
return (
|
|
<div
|
|
key={i}
|
|
className={cn(
|
|
"h-full transition-colors",
|
|
isActive ? "bg-blue-500" : "bg-gray-100"
|
|
)}
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
<p className="text-xs text-center text-gray-500">
|
|
{COLUMN_SPAN_VALUES[selectedComponent.gridColumnSpan || "half"]} / 12 컬럼
|
|
</p>
|
|
</div>
|
|
</div>;
|
|
|
|
{
|
|
/* 🆕 시작 컬럼 (고급 설정) */
|
|
}
|
|
<Collapsible>
|
|
<CollapsibleTrigger asChild>
|
|
<Button variant="ghost" size="sm" className="w-full justify-between">
|
|
<span>고급 설정</span>
|
|
<ChevronDown className="h-4 w-4" />
|
|
</Button>
|
|
</CollapsibleTrigger>
|
|
<CollapsibleContent className="mt-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="text-xs text-gray-500 mt-1">
|
|
"자동"을 선택하면 이전 컴포넌트 다음에 배치됩니다
|
|
</p>
|
|
</div>
|
|
</CollapsibleContent>
|
|
</Collapsible>;
|
|
|
|
{
|
|
/* ✅ 높이는 유지 */
|
|
}
|
|
<div>
|
|
<Label htmlFor="height" className="text-sm font-medium">
|
|
높이 (px)
|
|
</Label>
|
|
<Input
|
|
id="height"
|
|
type="number"
|
|
value={localInputs.height}
|
|
onChange={(e) => {
|
|
const newValue = e.target.value;
|
|
setLocalInputs((prev) => ({ ...prev, height: newValue }));
|
|
onUpdateProperty("size.height", Number(newValue));
|
|
}}
|
|
className="mt-1"
|
|
/>
|
|
</div>;
|
|
```
|
|
|
|
#### 2.2 사이드바 너비 설정 제거
|
|
|
|
**위치**: PropertiesPanel.tsx 라인 1081-1092
|
|
|
|
**변경 전**:
|
|
|
|
```typescript
|
|
<div>
|
|
<Label className="text-xs">사이드바 너비 (px)</Label>
|
|
<Input
|
|
type="number"
|
|
min="100"
|
|
value={
|
|
(selectedComponent as AreaComponent).layoutConfig?.sidebarWidth || 200
|
|
}
|
|
onChange={(e) => {
|
|
const value = Number(e.target.value);
|
|
onUpdateProperty("layoutConfig.sidebarWidth", value);
|
|
}}
|
|
className="mt-1"
|
|
/>
|
|
</div>
|
|
```
|
|
|
|
**변경 후**:
|
|
|
|
```typescript
|
|
<div>
|
|
<Label className="text-xs">사이드바 크기</Label>
|
|
<Select
|
|
value={
|
|
(selectedComponent as AreaComponent).layoutConfig?.sidebarSpan ||
|
|
"quarter"
|
|
}
|
|
onValueChange={(value) => {
|
|
onUpdateProperty("layoutConfig.sidebarSpan", value as ColumnSpanPreset);
|
|
}}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="small">작게 (2/12 - 17%)</SelectItem>
|
|
<SelectItem value="quarter">1/4 (3/12 - 25%)</SelectItem>
|
|
<SelectItem value="third">1/3 (4/12 - 33%)</SelectItem>
|
|
<SelectItem value="half">절반 (6/12 - 50%)</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
```
|
|
|
|
---
|
|
|
|
### Phase 3: 렌더링 로직 수정
|
|
|
|
#### 3.1 ContainerComponent 수정
|
|
|
|
**파일**: `frontend/components/screen/layout/ContainerComponent.tsx`
|
|
|
|
**변경 전**:
|
|
|
|
```typescript
|
|
const style: React.CSSProperties = {
|
|
gridColumn: `span ${component.size.width}`, // ❌ width 사용
|
|
minHeight: `${component.size.height}px`,
|
|
// ...
|
|
};
|
|
```
|
|
|
|
**변경 후**:
|
|
|
|
```typescript
|
|
const style: React.CSSProperties = {
|
|
// 🆕 gridColumnSpan 사용
|
|
gridColumn: component.gridColumnStart
|
|
? `${component.gridColumnStart} / span ${
|
|
COLUMN_SPAN_VALUES[component.gridColumnSpan]
|
|
}`
|
|
: `span ${COLUMN_SPAN_VALUES[component.gridColumnSpan]}`,
|
|
minHeight: `${component.size.height}px`,
|
|
// style.width는 제거
|
|
...(component.style && {
|
|
// width: component.style.width, ❌ 제거
|
|
height: component.style.height,
|
|
margin: component.style.margin,
|
|
padding: component.style.padding,
|
|
// ... 나머지
|
|
}),
|
|
};
|
|
```
|
|
|
|
#### 3.2 RealtimePreview 수정
|
|
|
|
**파일**: `frontend/components/screen/RealtimePreviewDynamic.tsx`
|
|
|
|
**추가**:
|
|
|
|
```typescript
|
|
// 컴포넌트 wrapper에 그리드 클래스 적용
|
|
const gridClasses = useMemo(() => {
|
|
if (!component.gridColumnSpan) return "";
|
|
|
|
const spanValue = COLUMN_SPAN_VALUES[component.gridColumnSpan];
|
|
const classes = [`col-span-${spanValue}`];
|
|
|
|
if (component.gridColumnStart) {
|
|
classes.push(`col-start-${component.gridColumnStart}`);
|
|
}
|
|
|
|
return classes.join(" ");
|
|
}, [component.gridColumnSpan, component.gridColumnStart]);
|
|
|
|
return (
|
|
<div className={cn(gridClasses /* 기타 클래스 */)}>
|
|
{/* 컴포넌트 렌더링 */}
|
|
</div>
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
### Phase 4: StyleEditor 수정
|
|
|
|
#### 4.1 width 스타일 제거
|
|
|
|
**파일**: `frontend/components/screen/ScreenDesigner.tsx` (라인 3874-3891)
|
|
|
|
**변경 전**:
|
|
|
|
```typescript
|
|
// 크기가 변경된 경우 component.size도 업데이트
|
|
if (newStyle.width || newStyle.height) {
|
|
const width = newStyle.width
|
|
? parseInt(newStyle.width.replace("px", ""))
|
|
: selectedComponent.size.width;
|
|
const height = newStyle.height
|
|
? parseInt(newStyle.height.replace("px", ""))
|
|
: selectedComponent.size.height;
|
|
|
|
updateComponentProperty(selectedComponent.id, "size.width", width);
|
|
updateComponentProperty(selectedComponent.id, "size.height", height);
|
|
}
|
|
```
|
|
|
|
**변경 후**:
|
|
|
|
```typescript
|
|
// 높이만 업데이트 (너비는 gridColumnSpan으로 제어)
|
|
if (newStyle.height) {
|
|
const height = parseInt(newStyle.height.replace("px", ""));
|
|
updateComponentProperty(selectedComponent.id, "size.height", height);
|
|
}
|
|
```
|
|
|
|
#### 4.2 StyleEditor 컴포넌트 자체 수정
|
|
|
|
**파일**: `frontend/components/screen/StyleEditor.tsx` (추정)
|
|
|
|
```typescript
|
|
// width 관련 탭/입력 제거
|
|
// ❌ 제거 대상:
|
|
// - 너비 입력 필드
|
|
// - min-width, max-width 설정
|
|
// - width 관련 모든 스타일 옵션
|
|
|
|
// ✅ 유지:
|
|
// - height 입력 필드
|
|
// - min-height, max-height 설정
|
|
```
|
|
|
|
---
|
|
|
|
### Phase 5: 템플릿 시스템 수정
|
|
|
|
#### 5.1 TemplateComponent 타입 수정
|
|
|
|
**파일**: `frontend/components/screen/panels/TemplatesPanel.tsx`
|
|
|
|
**변경 전**:
|
|
|
|
```typescript
|
|
export interface TemplateComponent {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
category: string;
|
|
icon: React.ReactNode;
|
|
defaultSize: { width: number; height: number }; // ❌
|
|
components: Array<{
|
|
type: string;
|
|
size: { width: number; height: number }; // ❌
|
|
// ...
|
|
}>;
|
|
}
|
|
```
|
|
|
|
**변경 후**:
|
|
|
|
```typescript
|
|
export interface TemplateComponent {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
category: string;
|
|
icon: React.ReactNode;
|
|
defaultSize: { height: number }; // ✅ width 제거
|
|
components: Array<{
|
|
type: string;
|
|
gridColumnSpan: ColumnSpanPreset; // 🆕 추가
|
|
gridColumnStart?: number; // 🆕 추가
|
|
size: { height: number }; // ✅ width 제거
|
|
// ...
|
|
}>;
|
|
}
|
|
```
|
|
|
|
#### 5.2 기본 템플릿 정의 수정
|
|
|
|
**예시 - 폼 템플릿**:
|
|
|
|
```typescript
|
|
const formTemplates: TemplateComponent[] = [
|
|
{
|
|
id: "basic-form-row",
|
|
name: "기본 폼 행",
|
|
description: "라벨 + 입력 필드",
|
|
category: "form",
|
|
icon: <FormInput />,
|
|
defaultSize: { height: 40 },
|
|
components: [
|
|
{
|
|
type: "widget",
|
|
widgetType: "text",
|
|
label: "라벨",
|
|
gridColumnSpan: "label", // 3/12
|
|
size: { height: 40 },
|
|
position: { x: 0, y: 0 },
|
|
},
|
|
{
|
|
type: "widget",
|
|
widgetType: "text",
|
|
placeholder: "입력하세요",
|
|
gridColumnSpan: "input", // 9/12
|
|
gridColumnStart: 4, // 4번 컬럼부터 시작
|
|
size: { height: 40 },
|
|
position: { x: 0, y: 0 },
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "two-column-form",
|
|
name: "2컬럼 폼",
|
|
description: "2개의 입력 필드를 나란히",
|
|
category: "form",
|
|
icon: <Columns />,
|
|
defaultSize: { height: 40 },
|
|
components: [
|
|
{
|
|
type: "widget",
|
|
widgetType: "text",
|
|
placeholder: "왼쪽 입력",
|
|
gridColumnSpan: "half", // 6/12
|
|
size: { height: 40 },
|
|
position: { x: 0, y: 0 },
|
|
},
|
|
{
|
|
type: "widget",
|
|
widgetType: "text",
|
|
placeholder: "오른쪽 입력",
|
|
gridColumnSpan: "half", // 6/12
|
|
gridColumnStart: 7, // 7번 컬럼부터 시작
|
|
size: { height: 40 },
|
|
position: { x: 0, y: 0 },
|
|
},
|
|
],
|
|
},
|
|
];
|
|
```
|
|
|
|
---
|
|
|
|
### Phase 6: 데이터 마이그레이션
|
|
|
|
#### 6.1 기존 데이터 변환 함수
|
|
|
|
```typescript
|
|
// lib/utils/widthToColumnSpan.ts
|
|
|
|
import {
|
|
ColumnSpanPreset,
|
|
COLUMN_SPAN_VALUES,
|
|
} from "@/types/screen-management";
|
|
|
|
/**
|
|
* 기존 픽셀 width를 가장 가까운 ColumnSpanPreset으로 변환
|
|
*/
|
|
export function convertWidthToColumnSpan(
|
|
width: number,
|
|
canvasWidth: number = 1920
|
|
): ColumnSpanPreset {
|
|
const percentage = (width / canvasWidth) * 100;
|
|
|
|
// 각 프리셋의 백분율 계산
|
|
const presetPercentages: Array<[ColumnSpanPreset, number]> = [
|
|
["full", 100],
|
|
["threeQuarters", 75],
|
|
["twoThirds", 67],
|
|
["half", 50],
|
|
["third", 33],
|
|
["quarter", 25],
|
|
["label", 25],
|
|
["input", 75],
|
|
["small", 17],
|
|
["medium", 33],
|
|
["large", 67],
|
|
];
|
|
|
|
// 가장 가까운 값 찾기
|
|
let closestPreset: ColumnSpanPreset = "half";
|
|
let minDiff = Infinity;
|
|
|
|
for (const [preset, presetPercentage] of presetPercentages) {
|
|
const diff = Math.abs(percentage - presetPercentage);
|
|
if (diff < minDiff) {
|
|
minDiff = diff;
|
|
closestPreset = preset;
|
|
}
|
|
}
|
|
|
|
return closestPreset;
|
|
}
|
|
|
|
/**
|
|
* 컴포넌트 배열에서 width를 gridColumnSpan으로 일괄 변환
|
|
*/
|
|
export function migrateComponentsToColumnSpan(
|
|
components: ComponentData[],
|
|
canvasWidth: number = 1920
|
|
): ComponentData[] {
|
|
return components.map((component) => {
|
|
const gridColumnSpan = convertWidthToColumnSpan(
|
|
component.size.width,
|
|
canvasWidth
|
|
);
|
|
|
|
return {
|
|
...component,
|
|
gridColumnSpan,
|
|
gridRowIndex: 0, // 초기값 (나중에 Y 좌표로 계산)
|
|
size: {
|
|
height: component.size.height,
|
|
// width 제거
|
|
},
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Y 좌표를 기준으로 행 인덱스 계산
|
|
*/
|
|
export function calculateRowIndices(
|
|
components: ComponentData[]
|
|
): ComponentData[] {
|
|
// Y 좌표로 정렬
|
|
const sorted = [...components].sort((a, b) => a.position.y - b.position.y);
|
|
|
|
let currentRowIndex = 0;
|
|
let currentY = sorted[0]?.position.y ?? 0;
|
|
const threshold = 50; // 50px 차이 이내는 같은 행
|
|
|
|
return sorted.map((component) => {
|
|
if (Math.abs(component.position.y - currentY) > threshold) {
|
|
currentRowIndex++;
|
|
currentY = component.position.y;
|
|
}
|
|
|
|
return {
|
|
...component,
|
|
gridRowIndex: currentRowIndex,
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 전체 레이아웃 마이그레이션
|
|
*/
|
|
export function migrateLayoutToGridSystem(layout: LayoutData): LayoutData {
|
|
console.log("🔄 레이아웃 마이그레이션 시작:", layout);
|
|
|
|
// 1단계: width를 gridColumnSpan으로 변환
|
|
let migratedComponents = migrateComponentsToColumnSpan(layout.components);
|
|
|
|
// 2단계: Y 좌표로 행 인덱스 계산
|
|
migratedComponents = calculateRowIndices(migratedComponents);
|
|
|
|
// 3단계: 같은 행 내에서 X 좌표로 gridColumnStart 계산
|
|
migratedComponents = calculateColumnStarts(migratedComponents);
|
|
|
|
console.log("✅ 마이그레이션 완료:", migratedComponents);
|
|
|
|
return {
|
|
...layout,
|
|
components: migratedComponents,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 같은 행 내에서 X 좌표로 시작 컬럼 계산
|
|
*/
|
|
function calculateColumnStarts(components: ComponentData[]): ComponentData[] {
|
|
// 행별로 그룹화
|
|
const rowGroups = new Map<number, ComponentData[]>();
|
|
|
|
for (const component of components) {
|
|
const rowIndex = component.gridRowIndex;
|
|
if (!rowGroups.has(rowIndex)) {
|
|
rowGroups.set(rowIndex, []);
|
|
}
|
|
rowGroups.get(rowIndex)!.push(component);
|
|
}
|
|
|
|
// 각 행 내에서 X 좌표로 정렬하고 시작 컬럼 계산
|
|
const result: ComponentData[] = [];
|
|
|
|
for (const [rowIndex, rowComponents] of rowGroups) {
|
|
// X 좌표로 정렬
|
|
const sorted = rowComponents.sort((a, b) => a.position.x - b.position.x);
|
|
|
|
let currentColumn = 1;
|
|
|
|
for (const component of sorted) {
|
|
result.push({
|
|
...component,
|
|
gridColumnStart: currentColumn,
|
|
});
|
|
|
|
// 다음 컴포넌트는 현재 컴포넌트 뒤에 배치
|
|
currentColumn += COLUMN_SPAN_VALUES[component.gridColumnSpan];
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
```
|
|
|
|
#### 6.2 자동 마이그레이션 실행
|
|
|
|
```typescript
|
|
// lib/api/screen.ts 또는 적절한 위치
|
|
|
|
/**
|
|
* 화면 로드 시 자동으로 마이그레이션 체크 및 실행
|
|
*/
|
|
export async function loadScreenLayoutWithMigration(
|
|
screenId: number
|
|
): Promise<LayoutData> {
|
|
const layout = await screenApi.getLayout(screenId);
|
|
|
|
// 마이그레이션 필요 여부 체크
|
|
const needsMigration = layout.components.some(
|
|
(c) => !c.gridColumnSpan || c.size.width !== undefined
|
|
);
|
|
|
|
if (needsMigration) {
|
|
console.log("🔄 자동 마이그레이션 실행:", screenId);
|
|
|
|
const migratedLayout = migrateLayoutToGridSystem(layout);
|
|
|
|
// 마이그레이션된 레이아웃 저장
|
|
await screenApi.saveLayout(screenId, migratedLayout);
|
|
|
|
return migratedLayout;
|
|
}
|
|
|
|
return layout;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Phase 7: Tailwind 설정 업데이트
|
|
|
|
#### 7.1 safelist 추가
|
|
|
|
```javascript
|
|
// tailwind.config.js
|
|
|
|
module.exports = {
|
|
// ... 기존 설정
|
|
|
|
safelist: [
|
|
// 그리드 컬럼 스팬 (1-12)
|
|
...Array.from({ length: 12 }, (_, i) => `col-span-${i + 1}`),
|
|
|
|
// 그리드 시작 위치 (1-12)
|
|
...Array.from({ length: 12 }, (_, i) => `col-start-${i + 1}`),
|
|
|
|
// 반응형 (필요시)
|
|
...Array.from({ length: 12 }, (_, i) => `md:col-span-${i + 1}`),
|
|
...Array.from({ length: 12 }, (_, i) => `lg:col-span-${i + 1}`),
|
|
],
|
|
|
|
// ... 나머지 설정
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## 📋 수정 파일 목록
|
|
|
|
### 필수 수정 파일
|
|
|
|
1. ✏️ `frontend/types/screen-management.ts` - 타입 정의 수정
|
|
2. ✏️ `frontend/components/screen/panels/PropertiesPanel.tsx` - width UI 제거
|
|
3. ✏️ `frontend/components/screen/layout/ContainerComponent.tsx` - 렌더링 수정
|
|
4. ✏️ `frontend/components/screen/layout/ColumnComponent.tsx` - 렌더링 수정
|
|
5. ✏️ `frontend/components/screen/layout/RowComponent.tsx` - 렌더링 수정
|
|
6. ✏️ `frontend/components/screen/ScreenDesigner.tsx` - StyleEditor 로직 수정
|
|
7. ✏️ `frontend/components/screen/StyleEditor.tsx` - width 옵션 제거
|
|
8. ✏️ `frontend/components/screen/panels/TemplatesPanel.tsx` - 템플릿 정의 수정
|
|
9. ✏️ `frontend/components/screen/RealtimePreviewDynamic.tsx` - 그리드 클래스 추가
|
|
10. ✏️ `frontend/components/screen/InteractiveScreenViewerDynamic.tsx` - 그리드 클래스 추가
|
|
|
|
### 새로 생성할 파일
|
|
|
|
11. 🆕 `frontend/lib/utils/widthToColumnSpan.ts` - 마이그레이션 유틸리티
|
|
12. 🆕 `frontend/lib/constants/columnSpans.ts` - 컬럼 스팬 상수 정의
|
|
|
|
### 추가 검토 필요
|
|
|
|
13. ⚠️ `frontend/components/screen/panels/DataTableConfigPanel.tsx` - 모달 width
|
|
14. ⚠️ `frontend/components/screen/panels/DetailSettingsPanel.tsx` - 확인 필요
|
|
15. ⚠️ `frontend/components/screen/FloatingPanel.tsx` - 패널 자체 width는 유지
|
|
|
|
---
|
|
|
|
## ✅ 단계별 체크리스트
|
|
|
|
### Phase 1: 타입 시스템 (Day 1)
|
|
|
|
- [ ] Size 인터페이스에서 width 제거
|
|
- [ ] BaseComponent에 gridColumnSpan 추가
|
|
- [ ] ColumnSpanPreset 타입 정의
|
|
- [ ] COLUMN_SPAN_VALUES 상수 정의
|
|
|
|
### Phase 2: UI 컴포넌트 (Day 2-3)
|
|
|
|
- [ ] PropertiesPanel - width 입력 → 컬럼 스팬 선택으로 변경
|
|
- [ ] PropertiesPanel - 시각적 프리뷰 추가
|
|
- [ ] PropertiesPanel - 사이드바 너비 → 컬럼 스팬으로 변경
|
|
- [ ] StyleEditor - width 옵션 완전 제거
|
|
|
|
### Phase 3: 렌더링 로직 (Day 3-4)
|
|
|
|
- [ ] ContainerComponent - gridColumn 계산 로직 수정
|
|
- [ ] ColumnComponent - gridColumn 계산 로직 수정
|
|
- [ ] RowComponent - gridColumn 계산 로직 수정
|
|
- [ ] RealtimePreview - 그리드 클래스 적용
|
|
- [ ] InteractiveScreenViewer - 그리드 클래스 적용
|
|
|
|
### Phase 4: 템플릿 시스템 (Day 4-5)
|
|
|
|
- [ ] TemplateComponent 타입 수정
|
|
- [ ] 모든 기본 템플릿 정의 업데이트
|
|
- [ ] 템플릿 적용 로직 수정
|
|
|
|
### Phase 5: 데이터 마이그레이션 (Day 5-6)
|
|
|
|
- [ ] widthToColumnSpan 유틸리티 작성
|
|
- [ ] 마이그레이션 함수 작성
|
|
- [ ] 자동 마이그레이션 적용
|
|
- [ ] 기존 화면 데이터 변환 테스트
|
|
|
|
### Phase 6: 테스트 및 검증 (Day 6-7)
|
|
|
|
- [ ] 새 컴포넌트 생성 테스트
|
|
- [ ] 기존 화면 로드 테스트
|
|
- [ ] 컬럼 스팬 변경 테스트
|
|
- [ ] 템플릿 적용 테스트
|
|
- [ ] 반응형 동작 확인
|
|
|
|
### Phase 7: Tailwind 설정 (Day 7)
|
|
|
|
- [ ] safelist 추가
|
|
- [ ] 불필요한 width 관련 유틸리티 제거
|
|
- [ ] 빌드 테스트
|
|
|
|
---
|
|
|
|
## ⚠️ 주의사항
|
|
|
|
### 1. 호환성 유지
|
|
|
|
- 기존 화면 데이터는 자동 마이그레이션
|
|
- 마이그레이션 전 백업 필수
|
|
- 단계적 배포 권장
|
|
|
|
### 2. 모달/팝업 크기
|
|
|
|
- 모달 크기는 컬럼 스팬이 아닌 기존 방식 유지
|
|
- `sm`, `md`, `lg`, `xl` 등의 사이즈 프리셋 사용
|
|
|
|
### 3. FloatingPanel
|
|
|
|
- 편집 패널 자체의 width는 유지
|
|
- 캔버스 내 컴포넌트만 컬럼 스팬 적용
|
|
|
|
### 4. 특수 케이스
|
|
|
|
- 데이터 테이블: 전체 너비(full) 고정
|
|
- 파일 업로드: 설정에 따라 다름
|
|
- 버튼: small, medium, large 프리셋 제공
|
|
|
|
---
|
|
|
|
## 🎯 완료 후 기대 효과
|
|
|
|
### ✅ 개선점
|
|
|
|
1. **일관성**: 모든 컴포넌트가 12컬럼 그리드 기반
|
|
2. **단순성**: 복잡한 픽셀 계산 불필요
|
|
3. **반응형**: Tailwind 표준으로 자동 대응
|
|
4. **유지보수**: width 관련 버그 완전 제거
|
|
5. **성능**: 불필요한 계산 로직 제거
|
|
|
|
### ❌ 제거되는 기능
|
|
|
|
- 픽셀 단위 정밀 너비 조정
|
|
- 자유로운 width 입력
|
|
- 커스텀 width 설정
|
|
|
|
### 🔄 대체 방안
|
|
|
|
- 정밀 조정 필요 시 → 컬럼 스팬 조합 사용
|
|
- 특수 케이스 → 커스텀 CSS 클래스 추가
|
|
|
|
---
|
|
|
|
## 📊 마이그레이션 타임라인
|
|
|
|
```
|
|
Week 1:
|
|
- Day 1-2: 타입 시스템 및 UI 컴포넌트 수정
|
|
- Day 3-4: 렌더링 로직 수정
|
|
- Day 5-6: 템플릿 및 마이그레이션
|
|
- Day 7: 테스트 및 Tailwind 설정
|
|
|
|
Week 2:
|
|
- 전체 시스템 통합 테스트
|
|
- 기존 화면 마이그레이션
|
|
- 문서화 및 배포
|
|
```
|
|
|
|
---
|
|
|
|
이 계획을 따르면 **width 속성을 완전히 제거**하고 **컬럼 수로만 제어**하는 깔끔한 시스템을 구축할 수 있습니다! 🎯
|