Files
vexplor/docs/WIDTH_REMOVAL_MIGRATION_PLAN.md
2025-10-13 18:28:03 +09:00

26 KiB

🗑️ Width 속성 완전 제거 계획서

🎯 목표

현재 화면 관리 시스템에서 픽셀 기반 width 설정을 완전히 제거하고, 컬럼 수(gridColumnSpan)로만 제어하도록 변경

📊 현재 Width 사용 현황

1. 타입 정의에서의 width

// 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 새로운 타입 정의

// 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 제거)

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

변경 전:

// 라인 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>

변경 후:

{
  /* 🆕 컬럼 스팬 선택 */
}
<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

변경 전:

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

변경 후:

<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

변경 전:

const style: React.CSSProperties = {
  gridColumn: `span ${component.size.width}`, // ❌ width 사용
  minHeight: `${component.size.height}px`,
  // ...
};

변경 후:

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

추가:

// 컴포넌트 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)

변경 전:

// 크기가 변경된 경우 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);
}

변경 후:

// 높이만 업데이트 (너비는 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 (추정)

// width 관련 탭/입력 제거
// ❌ 제거 대상:
// - 너비 입력 필드
// - min-width, max-width 설정
// - width 관련 모든 스타일 옵션

// ✅ 유지:
// - height 입력 필드
// - min-height, max-height 설정

Phase 5: 템플릿 시스템 수정

5.1 TemplateComponent 타입 수정

파일: frontend/components/screen/panels/TemplatesPanel.tsx

변경 전:

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 }; // ❌
    // ...
  }>;
}

변경 후:

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 기본 템플릿 정의 수정

예시 - 폼 템플릿:

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 기존 데이터 변환 함수

// 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 자동 마이그레이션 실행

// 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 추가

// 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 - 그리드 클래스 추가

새로 생성할 파일

  1. 🆕 frontend/lib/utils/widthToColumnSpan.ts - 마이그레이션 유틸리티
  2. 🆕 frontend/lib/constants/columnSpans.ts - 컬럼 스팬 상수 정의

추가 검토 필요

  1. ⚠️ frontend/components/screen/panels/DataTableConfigPanel.tsx - 모달 width
  2. ⚠️ frontend/components/screen/panels/DetailSettingsPanel.tsx - 확인 필요
  3. ⚠️ 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 속성을 완전히 제거하고 컬럼 수로만 제어하는 깔끔한 시스템을 구축할 수 있습니다! 🎯