# 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
``` | 문제 | 설명 | |------|------| | **축소만 됨** | 레이아웃 재배치 없음 | | **폰트 작아짐** | 전체 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
``` **변경 후**: ```tsx
``` ### 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 반응형을 구현할 수 있습니다.