689 lines
22 KiB
Markdown
689 lines
22 KiB
Markdown
|
|
# 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 반응형을 구현할 수 있습니다.
|