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

689 lines
22 KiB
Markdown
Raw Normal View History

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