# 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()` 방식
```tsx
// 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 변환 공식

```typescript
// 픽셀 → 퍼센트 변환
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축도 퍼센트 (권장)**
```typescript
const DESIGN_HEIGHT = 1080;
top: `${(position.y / DESIGN_HEIGHT) * 100}%`
```

**옵션 B: Y축은 픽셀 유지**
```typescript
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)**:
```tsx
const baseStyle = {
  left: `${adjustedPositionX}px`,
  top: `${position.y}px`,
  width: displayWidth,
  height: displayHeight,
  zIndex: component.type === "layout" ? 1 : position.z || 2,
};
```

**변경 후**:
```tsx
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)**:
```tsx
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,
};
```

**변경 후**:
```tsx
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)**:
```tsx
<div
  className="bg-background relative"
  style={{
    width: `${screenWidth}px`,
    height: `${screenHeight}px`,
    transform: `scale(${scale})`,
    transformOrigin: "top left",
    position: "relative",
  }}
>
```

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

### 4.5 공통 상수 파일 생성

```typescript
// 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 (수주 목록)
```json
{
  "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)**:
```typescript
const relativeChildComponent = {
  position: {
    x: child.position.x - component.position.x,
    y: child.position.y - component.position.y,
  },
};
```

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

**해결**: 부모 기준 퍼센트로 변환
```typescript
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)**:
```typescript
x: parseFloat(component.position?.x?.toString() || "0") - offsetX,
y: parseFloat(component.position?.y?.toString() || "0") - offsetY,
```

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

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

---

## 7. 잠재적 문제 및 해결책

### 7.1 최소 너비 문제

**문제**: 버튼이 너무 작아질 수 있음
```
158px 버튼 → 1280px 화면에서 105px
→ 텍스트가 잘릴 수 있음
```

**해결**: min-width 설정
```css
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분)

```typescript
// 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 반응형을 구현할 수 있습니다.
