ERP-node/docs/WIDTH_REMOVAL_MIGRATION_PLA...

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 속성을 완전히 제거**하고 **컬럼 수로만 제어**하는 깔끔한 시스템을 구축할 수 있습니다! 🎯