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시간)
- import 추가
- baseStyle의 left, width를 퍼센트로 변경
- 분할 패널 위 버튼 조정 로직도 퍼센트 적용
Phase 3: AutoRegisteringComponentRenderer.ts 수정 (30분)
- import 추가
- getComponentStyle()의 left, width를 퍼센트로 변경
Phase 4: page.tsx 수정 (1시간)
- scale 로직 제거 또는 수정
- 컨테이너 width: 100%로 변경
- 자식 컴포넌트 상대 위치 계산 수정
Phase 5: 테스트 (1시간)
- 1920px 화면에서 기존 화면 정상 동작 확인
- 1280px 화면으로 축소 테스트
- 분할 패널 화면 테스트
- 디자인 모드 테스트
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 반응형을 구현할 수 있습니다.