ERP-node/docs/DDD1542/PC_RESPONSIVE_IMPLEMENTATIO...

22 KiB

PC 반응형 구현 계획서

작성일: 2026-01-30 목표: PC 환경 (1280px ~ 1920px)에서 완벽한 반응형 구현


1. 목표 정의

1.1 범위

환경 화면 크기 우선순위
PC (대형 모니터) 1920px 기준
PC (노트북) 1280px ~ 1440px 1순위
태블릿 768px ~ 1024px 2순위 (추후)
모바일 < 768px 3순위 (추후)

1.2 목표 동작

1920px 화면에서 디자인
    ↓
1280px 화면으로 축소
    ↓
컴포넌트들이 비율에 맞게 재배치 (위치, 크기 모두)
    ↓
레이아웃 깨지지 않음

1.3 성공 기준

  • 1920px에서 디자인한 화면이 1280px에서 정상 표시
  • 버튼이 화면 밖으로 나가지 않음
  • 테이블이 화면 너비에 맞게 조정됨
  • 분할 패널이 비율 유지하며 축소됨

2. 현재 시스템 분석

2.1 렌더링 흐름 (현재)

┌─────────────────────────────────────────────────────────────┐
│ 1. API 호출                                                  │
│    screenApi.getLayoutV2(screenId)                          │
│    → screen_layouts_v2.layout_data (JSONB)                  │
└─────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────┐
│ 2. 데이터 변환                                               │
│    convertV2ToLegacy(v2Response)                            │
│    → components 배열 (position, size 포함)                   │
└─────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────┐
│ 3. 스케일 계산 (page.tsx 라인 395-460)                       │
│    const designWidth = layout.screenResolution.width || 1200│
│    const newScale = containerWidth / designWidth            │
│    → 전체 화면을 scale()로 축소                              │
└─────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────┐
│ 4. 컴포넌트 렌더링 (RealtimePreviewDynamic.tsx 라인 524-536) │
│    left: `${position.x}px`  ← 픽셀 고정                      │
│    top: `${position.y}px`   ← 픽셀 고정                      │
│    position: absolute       ← 절대 위치                      │
└─────────────────────────────────────────────────────────────┘

2.2 현재 방식의 문제점

현재: transform: scale() 방식

// page.tsx 라인 515-520
<div style={{
  width: `${screenWidth}px`,      // 1920px 고정
  height: `${screenHeight}px`,    // 고정
  transform: `scale(${scale})`,   // 전체 축소
  transformOrigin: "top left",
}}>
문제 설명
축소만 됨 레이아웃 재배치 없음
폰트 작아짐 전체 scale로 폰트도 축소
클릭 영역 오차 scale 적용 시 클릭 위치 계산 오류 가능
진정한 반응형 아님 비율만 유지, 레이아웃 최적화 없음

2.3 position.x, position.y 사용 위치

파일 라인 용도
RealtimePreviewDynamic.tsx 524-526 컴포넌트 위치 스타일
AutoRegisteringComponentRenderer.ts 42-43 공통 컴포넌트 스타일
page.tsx 744-745 자식 컴포넌트 상대 위치
ScreenDesigner.tsx 2890-2894 드래그 앤 드롭 위치
ScreenModal.tsx 620-621 모달 내 오프셋 조정

3. 구현 방식: 퍼센트 기반 배치

3.1 핵심 아이디어

픽셀 좌표 (1920px 기준)
    ↓
퍼센트로 변환
    ↓
화면 크기에 관계없이 비율 유지

예시:

버튼 위치: x=1753px (1920px 기준)
    ↓
퍼센트: 1753 / 1920 = 91.3%
    ↓
1280px 화면: 1280 * 0.913 = 1168px
    ↓
버튼이 화면 안에 정상 표시

3.2 변환 공식

// 픽셀 → 퍼센트 변환
const DESIGN_WIDTH = 1920;

function toPercent(pixelX: number): string {
  return `${(pixelX / DESIGN_WIDTH) * 100}%`;
}

// 사용
left: toPercent(position.x)  // "91.3%"
width: toPercent(size.width) // "8.2%"

3.3 Y축 처리

Y축은 두 가지 옵션:

옵션 A: Y축도 퍼센트 (권장)

const DESIGN_HEIGHT = 1080;
top: `${(position.y / DESIGN_HEIGHT) * 100}%`

옵션 B: Y축은 픽셀 유지

top: `${position.y}px`  // 세로는 스크롤로 해결

결정: 옵션 B (Y축 픽셀 유지)

  • 이유: 세로 스크롤은 자연스러움
  • 가로만 반응형이면 PC 환경에서 충분

4. 구현 상세

4.1 수정 파일 목록

파일 수정 내용
RealtimePreviewDynamic.tsx left, width를 퍼센트로 변경
AutoRegisteringComponentRenderer.ts left, width를 퍼센트로 변경
page.tsx scale 제거, 컨테이너 width: 100%

4.2 RealtimePreviewDynamic.tsx 수정

현재 (라인 524-530):

const baseStyle = {
  left: `${adjustedPositionX}px`,
  top: `${position.y}px`,
  width: displayWidth,
  height: displayHeight,
  zIndex: component.type === "layout" ? 1 : position.z || 2,
};

변경 후:

const DESIGN_WIDTH = 1920;

const baseStyle = {
  left: `${(adjustedPositionX / DESIGN_WIDTH) * 100}%`,  // 퍼센트
  top: `${position.y}px`,                                 // Y축은 픽셀 유지
  width: `${(parseFloat(displayWidth) / DESIGN_WIDTH) * 100}%`,  // 퍼센트
  height: displayHeight,                                  // 높이는 픽셀 유지
  zIndex: component.type === "layout" ? 1 : position.z || 2,
};

4.3 AutoRegisteringComponentRenderer.ts 수정

현재 (라인 40-48):

const baseStyle: React.CSSProperties = {
  position: "absolute",
  left: `${component.position?.x || 0}px`,
  top: `${component.position?.y || 0}px`,
  width: `${component.size?.width || 200}px`,
  height: `${component.size?.height || 36}px`,
  zIndex: component.position?.z || 1,
};

변경 후:

const DESIGN_WIDTH = 1920;

const baseStyle: React.CSSProperties = {
  position: "absolute",
  left: `${((component.position?.x || 0) / DESIGN_WIDTH) * 100}%`,     // 퍼센트
  top: `${component.position?.y || 0}px`,                               // Y축은 픽셀 유지
  width: `${((component.size?.width || 200) / DESIGN_WIDTH) * 100}%`,  // 퍼센트
  height: `${component.size?.height || 36}px`,                          // 높이는 픽셀 유지
  zIndex: component.position?.z || 1,
};

4.4 page.tsx 수정

현재 (라인 515-528):

<div
  className="bg-background relative"
  style={{
    width: `${screenWidth}px`,
    height: `${screenHeight}px`,
    transform: `scale(${scale})`,
    transformOrigin: "top left",
    position: "relative",
  }}
>

변경 후:

<div
  className="bg-background relative"
  style={{
    width: "100%",              // 전체 너비 사용
    minHeight: `${screenHeight}px`,  // 최소 높이
    position: "relative",
    // transform: scale 제거
  }}
>

4.5 공통 상수 파일 생성

// frontend/lib/constants/responsive.ts

export const RESPONSIVE_CONFIG = {
  DESIGN_WIDTH: 1920,
  DESIGN_HEIGHT: 1080,
  MIN_WIDTH: 1280,
  MAX_WIDTH: 1920,
} as const;

export function toPercentX(pixelX: number): string {
  return `${(pixelX / RESPONSIVE_CONFIG.DESIGN_WIDTH) * 100}%`;
}

export function toPercentWidth(pixelWidth: number): string {
  return `${(pixelWidth / RESPONSIVE_CONFIG.DESIGN_WIDTH) * 100}%`;
}

5. 가상 시뮬레이션

5.1 시뮬레이션 시나리오

테스트 화면: screen_id = 68 (수주 목록)

{
  "components": [
    {
      "id": "comp_1895",
      "url": "v2-table-list",
      "position": { "x": 8, "y": 128 },
      "size": { "width": 1904, "height": 600 }
    },
    {
      "id": "comp_1896",
      "url": "v2-button-primary",
      "position": { "x": 1753, "y": 88 },
      "size": { "width": 158, "height": 40 }
    },
    {
      "id": "comp_1897",
      "url": "v2-button-primary",
      "position": { "x": 1594, "y": 88 },
      "size": { "width": 158, "height": 40 }
    },
    {
      "id": "comp_1898",
      "url": "v2-button-primary",
      "position": { "x": 1436, "y": 88 },
      "size": { "width": 158, "height": 40 }
    }
  ]
}

5.2 현재 방식 시뮬레이션

1920px 화면:

┌────────────────────────────────────────────────────────────────────────┐
│                                          [분리] [저장] [수정] [삭제]    │
│                                          1277   1436   1594   1753     │
├────────────────────────────────────────────────────────────────────────┤
│ x=8                                                            x=1904  │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │                           테이블 (width: 1904px)                    │ │
│ └────────────────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────────┘
✅ 정상 표시

1280px 화면 (현재 scale 방식):

┌─────────────────────────────────────────────┐
│ scale(0.67) 적용                             │
│ ┌─────────────────────────────────────────┐ │
│ │                    [분리][저][수][삭]    │ │ ← 전체 축소, 폰트 작아짐
│ ├─────────────────────────────────────────┤ │
│ │ ┌─────────────────────────────────────┐ │ │
│ │ │       테이블 (축소됨)                 │ │ │
│ │ └─────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────┘ │
│                                             │
│  (여백 발생)                                 │
└─────────────────────────────────────────────┘
⚠️ 작동하지만 폰트/여백 문제

5.3 퍼센트 방식 시뮬레이션

변환 계산:

테이블:
  x: 8px → 8/1920 = 0.42%
  width: 1904px → 1904/1920 = 99.17%

삭제 버튼:
  x: 1753px → 1753/1920 = 91.30%
  width: 158px → 158/1920 = 8.23%

수정 버튼:
  x: 1594px → 1594/1920 = 83.02%
  width: 158px → 158/1920 = 8.23%

저장 버튼:
  x: 1436px → 1436/1920 = 74.79%
  width: 158px → 158/1920 = 8.23%

분리 버튼:
  x: 1277px → 1277/1920 = 66.51%
  width: 158px → 158/1920 = 8.23%

1920px 화면:

┌────────────────────────────────────────────────────────────────────────┐
│                                          [분리] [저장] [수정] [삭제]    │
│                                          66.5%  74.8%  83.0%  91.3%    │
├────────────────────────────────────────────────────────────────────────┤
│ 0.42%                                                           99.6%  │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │                           테이블 (width: 99.17%)                    │ │
│ └────────────────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────────┘
✅ 정상 표시 (1920px와 동일)

1280px 화면 (퍼센트 방식):

┌─────────────────────────────────────────────┐
│                    [분리][저장][수정][삭제]  │
│                    66.5% 74.8% 83.0% 91.3%  │
│                    = 851  957  1063  1169   │ ← 화면 안에 표시!
├─────────────────────────────────────────────┤
│ 0.42%                                99.6%  │
│ = 5px                               = 1275  │
│ ┌─────────────────────────────────────────┐ │
│ │         테이블 (width: 99.17%)          │ │ ← 화면 너비에 맞게 조정
│ │         = 1280 * 0.9917 = 1269px        │ │
│ └─────────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
✅ 비율 유지, 화면 안에 표시, 폰트 크기 유지

5.4 버튼 간격 검증

1920px:

분리: 1277px, 너비 158px → 끝: 1435px
저장: 1436px (간격: 1px)
수정: 1594px (간격: 1px)
삭제: 1753px (간격: 1px)

1280px (퍼센트 변환 후):

분리: 1280 * 0.665 = 851px, 너비 1280 * 0.082 = 105px → 끝: 956px
저장: 1280 * 0.748 = 957px (간격: 1px) ✅
수정: 1280 * 0.830 = 1063px (간격: 1px) ✅
삭제: 1280 * 0.913 = 1169px (간격: 1px) ✅

결론: 버튼 간격 비율도 유지됨


6. 엣지 케이스 검증

6.1 분할 패널 (SplitPanelLayout)

현재 동작:

  • 좌측 패널: 60% 너비
  • 우측 패널: 40% 너비
  • 이미 퍼센트 기반!

시뮬레이션:

1920px: 좌측 1152px, 우측 768px
1280px: 좌측 768px, 우측 512px
✅ 자동으로 비율 유지됨

분할 패널 내부 컴포넌트:

  • 문제: 내부 컴포넌트가 픽셀 고정이면 깨짐
  • 해결: 분할 패널 내부도 퍼센트 적용 필요

6.2 테이블 컴포넌트 (TableList)

현재:

  • 테이블 자체는 컨테이너 너비 100% 사용
  • 컬럼 너비는 내부적으로 조정

시뮬레이션:

1920px: 테이블 컨테이너 width: 99.17% = 1904px
1280px: 테이블 컨테이너 width: 99.17% = 1269px
✅ 테이블이 자동으로 조정됨

6.3 자식 컴포넌트 상대 위치

현재 코드 (page.tsx 라인 744-745):

const relativeChildComponent = {
  position: {
    x: child.position.x - component.position.x,
    y: child.position.y - component.position.y,
  },
};

문제: 상대 좌표도 픽셀 기반

해결: 부모 기준 퍼센트로 변환

const relativeChildComponent = {
  position: {
    // 부모 너비 기준 퍼센트
    xPercent: ((child.position.x - component.position.x) / component.size.width) * 100,
    y: child.position.y - component.position.y,
  },
};

6.4 드래그 앤 드롭 (디자인 모드)

ScreenDesigner.tsx:

  • 드롭 위치는 여전히 픽셀로 저장
  • 렌더링 시에만 퍼센트로 변환
  • 저장 방식 변경 없음!

시뮬레이션:

1. 디자이너가 1920px 화면에서 버튼 드롭
2. position: { x: 1753, y: 88 } 저장 (픽셀)
3. 렌더링 시 91.3%로 변환
4. 1280px 화면에서도 정상 표시
✅ 디자인 모드 호환

6.5 모달 내 화면

ScreenModal.tsx (라인 620-621):

x: parseFloat(component.position?.x?.toString() || "0") - offsetX,
y: parseFloat(component.position?.y?.toString() || "0") - offsetY,

문제: 오프셋 계산이 픽셀 기반

해결: 모달 컨테이너도 퍼센트 기반으로 변경

// 모달 컨테이너 너비 기준으로 퍼센트 계산
const modalWidth = containerRef.current?.clientWidth || DESIGN_WIDTH;
const xPercent = ((position.x - offsetX) / DESIGN_WIDTH) * 100;

7. 잠재적 문제 및 해결책

7.1 최소 너비 문제

문제: 버튼이 너무 작아질 수 있음

158px 버튼 → 1280px 화면에서 105px
→ 텍스트가 잘릴 수 있음

해결: min-width 설정

min-width: 80px;

7.2 겹침 문제

문제: 화면이 작아지면 컴포넌트가 겹칠 수 있음

시뮬레이션:

1920px: 버튼 4개가 간격 1px로 배치
1280px: 버튼 4개가 간격 1px로 배치 (비율 유지)
✅ 겹치지 않음 (간격도 비율로 축소)

7.3 폰트 크기

현재: 폰트는 px 고정 변경 후: 폰트 크기 유지 (scale이 아니므로)

결과: 폰트 크기는 그대로, 레이아웃만 비율 조정 가독성 유지

7.4 height 처리

결정: height는 픽셀 유지

  • 이유: 세로 스크롤은 자연스러움
  • 세로 반응형은 불필요 (PC 환경)

8. 호환성 검증

8.1 기존 화면 호환

항목 호환 여부 이유
일반 버튼 퍼센트로 변환, 위치 유지
테이블 컨테이너 비율 유지
분할 패널 이미 퍼센트 기반
탭 레이아웃 컨테이너 비율 유지
그리드 레이아웃 내부는 기존 방식
인풋 필드 컨테이너 비율 유지

8.2 디자인 모드 호환

항목 호환 여부 이유
드래그 앤 드롭 저장은 픽셀, 렌더링만 퍼센트
리사이즈 저장은 픽셀, 렌더링만 퍼센트
그리드 스냅 스냅은 픽셀 기준 유지
미리보기 렌더링 동일 방식

8.3 API 호환

항목 호환 여부 이유
DB 저장 구조 변경 없음 (픽셀 저장)
API 응답 구조 변경 없음
V2 변환 변환 로직 변경 없음

9. 구현 순서

Phase 1: 공통 유틸리티 생성 (30분)

// frontend/lib/constants/responsive.ts
export const RESPONSIVE_CONFIG = {
  DESIGN_WIDTH: 1920,
} as const;

export function toPercentX(pixelX: number): string {
  return `${(pixelX / RESPONSIVE_CONFIG.DESIGN_WIDTH) * 100}%`;
}

export function toPercentWidth(pixelWidth: number): string {
  return `${(pixelWidth / RESPONSIVE_CONFIG.DESIGN_WIDTH) * 100}%`;
}

Phase 2: RealtimePreviewDynamic.tsx 수정 (1시간)

  1. import 추가
  2. baseStyle의 left, width를 퍼센트로 변경
  3. 분할 패널 위 버튼 조정 로직도 퍼센트 적용

Phase 3: AutoRegisteringComponentRenderer.ts 수정 (30분)

  1. import 추가
  2. getComponentStyle()의 left, width를 퍼센트로 변경

Phase 4: page.tsx 수정 (1시간)

  1. scale 로직 제거 또는 수정
  2. 컨테이너 width: 100%로 변경
  3. 자식 컴포넌트 상대 위치 계산 수정

Phase 5: 테스트 (1시간)

  1. 1920px 화면에서 기존 화면 정상 동작 확인
  2. 1280px 화면으로 축소 테스트
  3. 분할 패널 화면 테스트
  4. 디자인 모드 테스트

10. 최종 체크리스트

구현 전

  • 현재 동작하는 화면 스크린샷 캡처 (비교용)
  • 테스트 화면 목록 선정

구현 중

  • responsive.ts 생성
  • RealtimePreviewDynamic.tsx 수정
  • AutoRegisteringComponentRenderer.ts 수정
  • page.tsx 수정

구현 후

  • 1920px 화면 테스트
  • 1440px 화면 테스트
  • 1280px 화면 테스트
  • 분할 패널 화면 테스트
  • 디자인 모드 테스트
  • 모달 내 화면 테스트

11. 예상 소요 시간

작업 시간
유틸리티 생성 30분
RealtimePreviewDynamic.tsx 1시간
AutoRegisteringComponentRenderer.ts 30분
page.tsx 1시간
테스트 1시간
합계 4시간

12. 결론

퍼센트 기반 배치가 PC 반응형의 가장 확실한 해결책입니다.

항목 scale 방식 퍼센트 방식
폰트 크기 축소됨 유지
레이아웃 비율 유지 유지
클릭 영역 오차 가능 정확
구현 복잡도 낮음 중간
진정한 반응형

DB 변경 없이, 렌더링 로직만 수정하여 완벽한 PC 반응형을 구현할 수 있습니다.