ERP-node/popdocs/archive/POP_V4_CONSTRAINT_SYSTEM_PL...

22 KiB

POP v4.0 제약조건 기반 시스템 구현 계획

1. 현재 시스템 분석

1.1 현재 구조 (v3.0)

// 4개 모드별 그리드 위치 기반
interface PopLayoutDataV3 {
  version: "pop-3.0";
  layouts: {
    tablet_landscape: { componentPositions: Record<string, GridPosition> };
    tablet_portrait: { componentPositions: Record<string, GridPosition> };
    mobile_landscape: { componentPositions: Record<string, GridPosition> };
    mobile_portrait: { componentPositions: Record<string, GridPosition> };
  };
  components: Record<string, PopComponentDefinition>;
  // ...
}

interface GridPosition {
  col: number;      // 1-based
  row: number;      // 1-based
  colSpan: number;
  rowSpan: number;
}

1.2 현재 문제점

  1. 4배 작업량: 4개 모드 각각 설계 필요
  2. 수동 동기화: 컴포넌트 추가/삭제 시 4모드 수동 동기화
  3. 그리드 한계: col/row 기반이라 자동 재배치 불가
  4. 반응형 미흡: 화면 크기 변화에 자동 적응 불가
  5. 디바이스 차이 무시: 태블릿/모바일 물리적 크기 차이 고려 안됨

2. 새로운 시스템 설계 (v4.0)

2.1 핵심 철학

"하나의 레이아웃 설계 → 제약조건 설정 → 모든 화면 자동 적응"
  • 단일 소스: 1개 레이아웃만 설계
  • 제약조건 기반: 컴포넌트가 "어떻게 반응할지" 규칙 정의
  • Flexbox 렌더링: CSS Grid에서 Flexbox 기반으로 전환
  • 자동 줄바꿈: 공간 부족 시 자동 재배치

2.2 새로운 데이터 구조

// v4.0 레이아웃
interface PopLayoutDataV4 {
  version: "pop-4.0";
  
  // 루트 컨테이너
  root: PopContainer;
  
  // 컴포넌트 정의 (ID → 정의)
  components: Record<string, PopComponentDefinitionV4>;
  
  // 데이터 흐름
  dataFlow: PopDataFlow;
  
  // 전역 설정
  settings: PopGlobalSettingsV4;
  
  // 메타데이터
  metadata?: PopLayoutMetadata;
}

2.3 컨테이너 (스택)

// 컨테이너: 컴포넌트들을 담는 그룹
interface PopContainer {
  id: string;
  type: "stack";
  
  // 스택 방향
  direction: "horizontal" | "vertical";
  
  // 줄바꿈 허용
  wrap: boolean;
  
  // 요소 간 간격
  gap: number;
  
  // 정렬
  alignItems: "start" | "center" | "end" | "stretch";
  justifyContent: "start" | "center" | "end" | "space-between" | "space-around";
  
  // 패딩
  padding?: {
    top: number;
    right: number;
    bottom: number;
    left: number;
  };
  
  // 반응형 규칙 (선택)
  responsive?: {
    // 브레이크포인트 (이 너비 이하에서 적용)
    breakpoint: number;
    // 변경할 방향
    direction?: "horizontal" | "vertical";
    // 변경할 간격
    gap?: number;
  }[];
  
  // 자식 요소 (컴포넌트 ID 또는 중첩 컨테이너)
  children: (string | PopContainer)[];
}

2.4 컴포넌트 제약조건

interface PopComponentDefinitionV4 {
  id: string;
  type: PopComponentType;
  label?: string;
  
  // ===== 크기 제약 (핵심) =====
  size: {
    // 너비 모드
    width: "fixed" | "fill" | "hug";
    // 높이 모드
    height: "fixed" | "fill" | "hug";
    
    // 고정 크기 (width/height가 fixed일 때)
    fixedWidth?: number;
    fixedHeight?: number;
    
    // 최소/최대 크기
    minWidth?: number;
    maxWidth?: number;
    minHeight?: number;
    maxHeight?: number;
    
    // 비율 (fill일 때, 같은 컨테이너 내 다른 요소와의 비율)
    flexGrow?: number;  // 기본 1
    flexShrink?: number; // 기본 1
  };
  
  // ===== 정렬 =====
  alignSelf?: "start" | "center" | "end" | "stretch";
  
  // ===== 여백 =====
  margin?: {
    top: number;
    right: number;
    bottom: number;
    left: number;
  };
  
  // ===== 모바일 스케일 (선택) =====
  // 모바일에서 컴포넌트를 더 크게 표시
  mobileScale?: number; // 기본 1.0, 예: 1.2 = 20% 더 크게
  
  // ===== 기존 속성 =====
  dataBinding?: PopDataBinding;
  style?: PopStylePreset;
  config?: PopComponentConfig;
}

2.5 크기 모드 설명

모드 설명 CSS 변환
fixed 고정 크기 (px) width: {fixedWidth}px
fill 부모 공간 채우기 flex: {flexGrow} {flexShrink} 0
hug 내용에 맞춤 flex: 0 0 auto

2.6 전역 설정

interface PopGlobalSettingsV4 {
  // 기본 터치 타겟 크기
  touchTargetMin: number; // 48px
  
  // 모드 (일반/산업현장)
  mode: "normal" | "industrial";
  
  // 기본 간격
  defaultGap: number; // 8px
  
  // 기본 패딩
  defaultPadding: number; // 16px
  
  // 반응형 브레이크포인트 (전역)
  breakpoints: {
    tablet: number;  // 768px
    mobile: number;  // 480px
  };
}

3. 디자이너 UI 변경

3.1 기존 디자이너 vs 새 디자이너

기존 (그리드 기반):
┌──────────────────────────────────────────────┐
│ [태블릿 가로] [태블릿 세로] [모바일 가로] [모바일 세로] │
│                                              │
│  24x24 그리드에 컴포넌트 드래그 배치          │
│                                              │
└──────────────────────────────────────────────┘

새로운 (제약조건 기반):
┌──────────────────────────────────────────────┐
│ [단일 캔버스]              미리보기: [태블릿▼] │
│                                              │
│  스택(컨테이너)에 컴포넌트 배치               │
│  + 우측 패널에서 제약조건 설정                │
│                                              │
└──────────────────────────────────────────────┘

3.2 새로운 디자이너 레이아웃

┌─────────────────────────────────────────────────────────────────┐
│ POP 화면 디자이너 v4                              [저장] [미리보기] │
├────────────────┬────────────────────────┬───────────────────────┤
│                │                        │                       │
│  컴포넌트      │     캔버스              │   속성 패널            │
│                │                        │                       │
│  ▼ 기본        │  ┌──────────────────┐  │  ▼ 선택됨: 입력창      │
│  [필드]        │  │ ┌──────────────┐ │  │                       │
│  [버튼]        │  │ │입력창        │ │  │  ▼ 크기              │
│  [리스트]      │  │ └──────────────┘ │  │  너비: [채우기 ▼]     │
│  [인디케이터]  │  │                  │  │  최소: [100] px       │
│                │  │ ┌─────┐ ┌─────┐ │  │  최대: [없음]         │
│  ▼ 입력        │  │ │버튼1│ │버튼2│ │  │                       │
│  [스캐너]      │  │ └─────┘ └─────┘ │  │  높이: [고정 ▼]       │
│  [숫자패드]    │  │                  │  │  값: [48] px          │
│                │  └──────────────────┘  │                       │
│  ─────────     │                        │  ▼ 정렬              │
│                │  미리보기:             │  [늘이기 ▼]           │
│  ▼ 레이아웃    │  ┌──────────────────┐  │                       │
│  [스택 (가로)] │  │[태블릿 가로 ▼]   │  │  ▼ 여백              │
│  [스택 (세로)] │  │[768px]           │  │  상[8] 우[0] 하[8] 좌[0]│
│                │  └──────────────────┘  │                       │
│                │                        │  ▼ 반응형             │
│                │                        │  모바일 스케일: [1.2]  │
│                │                        │                       │
└────────────────┴────────────────────────┴───────────────────────┘

3.3 컨테이너(스택) 편집

┌─ 스택 속성 ─────────────────────┐
│                                 │
│ 방향: [가로 ▼]                  │
│ 줄바꿈: [허용 ☑]                │
│ 간격: [8] px                    │
│                                 │
│ 정렬 (가로): [가운데 ▼]         │
│ 정렬 (세로): [늘이기 ▼]         │
│                                 │
│ ▼ 반응형 규칙                   │
│ ┌─────────────────────────────┐ │
│ │ 768px 이하: 세로 방향        │ │
│ │ [+ 규칙 추가]               │ │
│ └─────────────────────────────┘ │
│                                 │
└─────────────────────────────────┘

4. 렌더링 로직 변경

4.1 기존 렌더링 (CSS Grid)

// v3: CSS Grid 기반
<div style={{
  display: "grid",
  gridTemplateColumns: `repeat(24, 1fr)`,
  gridTemplateRows: `repeat(24, 1fr)`,
  gap: "4px",
}}>
  {componentIds.map(id => (
    <div style={{
      gridColumn: `${pos.col} / span ${pos.colSpan}`,
      gridRow: `${pos.row} / span ${pos.rowSpan}`,
    }}>
      <Component />
    </div>
  ))}
</div>

4.2 새로운 렌더링 (Flexbox)

// v4: Flexbox 기반
function renderContainer(container: PopContainer, components: Record<string, PopComponentDefinitionV4>) {
  const direction = useResponsiveValue(container, 'direction');
  const gap = useResponsiveValue(container, 'gap');
  
  return (
    <div style={{
      display: "flex",
      flexDirection: direction === "horizontal" ? "row" : "column",
      flexWrap: container.wrap ? "wrap" : "nowrap",
      gap: `${gap}px`,
      alignItems: container.alignItems,
      justifyContent: container.justifyContent,
      padding: container.padding ? 
        `${container.padding.top}px ${container.padding.right}px ${container.padding.bottom}px ${container.padding.left}px` 
        : undefined,
    }}>
      {container.children.map(child => {
        if (typeof child === "string") {
          // 컴포넌트 렌더링
          return renderComponent(components[child]);
        } else {
          // 중첩 컨테이너 렌더링
          return renderContainer(child, components);
        }
      })}
    </div>
  );
}

function renderComponent(component: PopComponentDefinitionV4) {
  const { size, margin, mobileScale } = component;
  const isMobile = useIsMobile();
  const scale = isMobile && mobileScale ? mobileScale : 1;
  
  // 크기 계산
  let width: string;
  let flex: string;
  
  if (size.width === "fixed") {
    width = `${(size.fixedWidth || 100) * scale}px`;
    flex = "0 0 auto";
  } else if (size.width === "fill") {
    width = "auto";
    flex = `${size.flexGrow || 1} ${size.flexShrink || 1} 0`;
  } else { // hug
    width = "auto";
    flex = "0 0 auto";
  }
  
  return (
    <div style={{
      flex,
      width,
      minWidth: size.minWidth ? `${size.minWidth * scale}px` : undefined,
      maxWidth: size.maxWidth ? `${size.maxWidth * scale}px` : undefined,
      height: size.height === "fixed" ? `${(size.fixedHeight || 48) * scale}px` : "auto",
      minHeight: size.minHeight ? `${size.minHeight * scale}px` : undefined,
      maxHeight: size.maxHeight ? `${size.maxHeight * scale}px` : undefined,
      margin: margin ? 
        `${margin.top}px ${margin.right}px ${margin.bottom}px ${margin.left}px` 
        : undefined,
      alignSelf: component.alignSelf,
    }}>
      <ActualComponent {...component} />
    </div>
  );
}

4.3 반응형 훅

function useResponsiveValue<T>(
  container: PopContainer, 
  property: keyof PopContainer
): T {
  const windowWidth = useWindowWidth();
  
  // 기본값
  let value = container[property] as T;
  
  // 반응형 규칙 적용 (작은 브레이크포인트 우선)
  if (container.responsive) {
    const sortedRules = [...container.responsive].sort((a, b) => b.breakpoint - a.breakpoint);
    for (const rule of sortedRules) {
      if (windowWidth <= rule.breakpoint && rule[property] !== undefined) {
        value = rule[property] as T;
      }
    }
  }
  
  return value;
}

5. 구현 단계

Phase 1: 데이터 구조 (1-2일)

파일: frontend/components/pop/designer/types/pop-layout.ts

  1. PopLayoutDataV4 인터페이스 정의
  2. PopContainer 인터페이스 정의
  3. PopComponentDefinitionV4 인터페이스 정의
  4. createEmptyPopLayoutV4() 함수
  5. migrateV3ToV4() 마이그레이션 함수
  6. ensureV4Layout() 함수
  7. 타입 가드 함수들

Phase 2: 렌더러 (2-3일)

파일: frontend/components/pop/designer/renderers/PopLayoutRendererV4.tsx

  1. renderContainer() 함수
  2. renderComponent() 함수
  3. useResponsiveValue()
  4. useWindowWidth()
  5. CSS 스타일 계산 로직
  6. 반응형 브레이크포인트 처리

Phase 3: 디자이너 UI (3-4일)

파일: frontend/components/pop/designer/PopDesignerV4.tsx

  1. 캔버스 영역 (드래그 앤 드롭)
  2. 컴포넌트 팔레트 (기존 + 스택)
  3. 속성 패널
    • 크기 제약 편집
    • 정렬 편집
    • 여백 편집
    • 반응형 규칙 편집
  4. 미리보기 모드 (다양한 화면 크기)
  5. 컨테이너(스택) 관리
    • 컨테이너 추가/삭제
    • 컨테이너 설정 편집
    • 컴포넌트 이동 (컨테이너 간)

Phase 4: 뷰어 통합 (1-2일)

파일: frontend/app/(pop)/pop/screens/[screenId]/page.tsx

  1. v4 레이아웃 감지 및 렌더링
  2. 기존 v3 호환 유지
  3. 반응형 모드 감지 연동
  4. 성능 최적화

Phase 5: 백엔드 수정 (1일)

파일: backend-node/src/services/screenManagementService.ts

  1. saveLayoutPop - v4 버전 감지 및 저장
  2. getLayoutPop - v4 버전 반환
  3. 버전 마이그레이션 로직

Phase 6: 테스트 및 마이그레이션 (2-3일)

  1. 단위 테스트
  2. 통합 테스트
  3. 기존 v3 레이아웃 마이그레이션 도구
  4. 크로스 디바이스 테스트

6. 마이그레이션 전략

6.1 v3 → v4 자동 변환

function migrateV3ToV4(v3: PopLayoutDataV3): PopLayoutDataV4 {
  // 태블릿 가로 모드 기준으로 변환
  const baseLayout = v3.layouts.tablet_landscape;
  const componentIds = Object.keys(baseLayout.componentPositions);
  
  // 컴포넌트를 row, col 순으로 정렬
  const sortedIds = componentIds.sort((a, b) => {
    const posA = baseLayout.componentPositions[a];
    const posB = baseLayout.componentPositions[b];
    if (posA.row !== posB.row) return posA.row - posB.row;
    return posA.col - posB.col;
  });
  
  // 같은 row에 있는 컴포넌트들을 가로 스택으로 그룹화
  const rowGroups = groupByRow(sortedIds, baseLayout.componentPositions);
  
  // 루트 컨테이너 (세로 스택)
  const rootContainer: PopContainer = {
    id: "root",
    type: "stack",
    direction: "vertical",
    wrap: false,
    gap: v3.settings.canvasGrid.gap,
    alignItems: "stretch",
    justifyContent: "start",
    children: [],
  };
  
  // 각 행을 가로 스택으로 변환
  for (const [row, ids] of rowGroups) {
    if (ids.length === 1) {
      // 단일 컴포넌트면 직접 추가
      rootContainer.children.push(ids[0]);
    } else {
      // 여러 컴포넌트면 가로 스택으로 감싸기
      const rowStack: PopContainer = {
        id: `row-${row}`,
        type: "stack",
        direction: "horizontal",
        wrap: true,
        gap: v3.settings.canvasGrid.gap,
        alignItems: "center",
        justifyContent: "start",
        children: ids,
      };
      rootContainer.children.push(rowStack);
    }
  }
  
  // 컴포넌트 정의 변환
  const components: Record<string, PopComponentDefinitionV4> = {};
  for (const id of componentIds) {
    const v3Comp = v3.components[id];
    const pos = baseLayout.componentPositions[id];
    
    components[id] = {
      ...v3Comp,
      size: {
        // colSpan을 기반으로 크기 모드 결정
        width: pos.colSpan >= 20 ? "fill" : "fixed",
        height: "fixed",
        fixedWidth: pos.colSpan * (1024 / 24), // 대략적인 픽셀 변환
        fixedHeight: pos.rowSpan * (768 / 24),
        minWidth: 100,
      },
    };
  }
  
  return {
    version: "pop-4.0",
    root: rootContainer,
    components,
    dataFlow: v3.dataFlow,
    settings: {
      touchTargetMin: v3.settings.touchTargetMin,
      mode: v3.settings.mode,
      defaultGap: v3.settings.canvasGrid.gap,
      defaultPadding: 16,
      breakpoints: {
        tablet: 768,
        mobile: 480,
      },
    },
    metadata: v3.metadata,
  };
}

6.2 하위 호환

  • v3 레이아웃은 계속 지원
  • 디자이너에서 v3 → v4 업그레이드 버튼 제공
  • 새로 생성하는 레이아웃은 v4

7. 예상 효과

7.1 사용자 경험

항목 기존 (v3) 새로운 (v4)
설계 개수 4개 1개
작업 시간 4배 1배
반응형 수동 자동
디바이스 대응 각각 설정 mobileScale

7.2 개발자 경험

항목 기존 (v3) 새로운 (v4)
렌더링 CSS Grid Flexbox
위치 계산 col/row 자동
반응형 로직 4모드 분기 브레이크포인트
유지보수 복잡 단순

8. 일정 (예상)

Phase 내용 기간
1 데이터 구조 1-2일
2 렌더러 2-3일
3 디자이너 UI 3-4일
4 뷰어 통합 1-2일
5 백엔드 수정 1일
6 테스트/마이그레이션 2-3일
총계 10-15일

9. 리스크 및 대응

9.1 기존 레이아웃 호환성

  • 리스크: v3 → v4 자동 변환이 완벽하지 않을 수 있음
  • 대응:
    • 마이그레이션 미리보기 기능
    • 수동 조정 도구 제공
    • v3 유지 옵션

9.2 학습 곡선

  • 리스크: 제약조건 개념이 익숙하지 않을 수 있음
  • 대응:
    • 프리셋 제공 (예: "화면 전체 채우기", "고정 크기")
    • 툴팁/도움말
    • 예제 템플릿

9.3 성능

  • 리스크: Flexbox 중첩으로 렌더링 성능 저하
  • 대응:
    • 컨테이너 중첩 깊이 제한 (최대 3-4)
    • React.memo 활용
    • 가상화 (리스트 컴포넌트)

10. 결론

v4.0 제약조건 기반 시스템은 업계 표준(Figma, Flutter, SwiftUI)을 따르며, 사용자의 작업량을 75% 줄이고 자동 반응형을 제공합니다.

구현 후 POP 디자이너는:

  • 1개 레이아웃만 설계
  • 모든 화면 크기에 자동 적응
  • 모바일 특화 설정 (mobileScale)으로 세밀한 제어 가능

11. 추가 설정 (2026-02-03 업데이트)

11.1 확장된 전역 설정

interface PopGlobalSettingsV4 {
  // 기존
  touchTargetMin: number;  // 48 (normal) / 60 (industrial)
  mode: "normal" | "industrial";
  defaultGap: number;
  defaultPadding: number;
  breakpoints: {
    tablet: number;  // 768
    mobile: number;  // 480
  };
  
  // 신규 추가
  environment: "indoor" | "outdoor";  // 야외면 대비 높임
  
  typography: {
    body: { min: number; max: number };     // 14-18px
    heading: { min: number; max: number };  // 18-28px
    caption: { min: number; max: number };  // 12-14px
  };
  
  contrast: "normal" | "high";  // outdoor면 자동 high
}

11.2 컴포넌트 기본값 프리셋

컴포넌트 추가 시 자동 적용되는 안전한 기본값:

const COMPONENT_DEFAULTS = {
  "pop-button": {
    minWidth: 80,
    minHeight: 48,
    height: "fixed",
    fixedHeight: 48,
  },
  "pop-field": {
    minWidth: 120,
    minHeight: 40,
    height: "fixed",
    fixedHeight: 48,
  },
  "pop-list": {
    minHeight: 200,
    itemHeight: 48,
  },
  // ...
};

11.3 리스트 반응형 컬럼

interface PopListConfig {
  // 기존
  listType: PopListType;
  displayColumns?: string[];
  
  // 신규 추가
  responsiveColumns?: {
    tablet: string[];   // 전체 컬럼
    mobile: string[];   // 주요 컬럼만
  };
}

11.4 라벨 배치 자동화

interface PopContainer {
  // 기존
  direction: "horizontal" | "vertical";
  
  // 신규 추가
  labelPlacement?: "auto" | "above" | "beside";
  // auto: 모바일 세로=위, 태블릿 가로=옆
}

12. 관련 문서


13. 현재 상태 (2026-02-03)

구현 대기: 컴포넌트가 아직 없어서 레이아웃 시스템보다 컴포넌트 개발이 선행되어야 함.

권장 진행 순서:

  1. 기초 컴포넌트 개발 (PopButton, PopInput 등)
  2. 조합 컴포넌트 개발 (PopFormField, PopCard 등)
  3. 복합 컴포넌트 개발 (PopDataTable, PopCardList 등)
  4. v4 레이아웃 시스템 구현
  5. 디자이너 UI 개발

최종 업데이트: 2026-02-03