feat: V2 레이아웃 API 통합 및 변환 로직 추가
- ScreenModal 컴포넌트에서 V2 레이아웃 API를 사용하여 화면 정보와 레이아웃 데이터를 로딩하도록 수정하였습니다. - V2 레이아웃 데이터를 Legacy 형식으로 변환하는 로직을 추가하여 기본값 병합을 지원합니다. - V2 레이아웃이 없을 경우 기존 API로 fallback하는 기능을 구현하였습니다. - 관련 문서 및 주석을 업데이트하여 새로운 기능에 대한 이해를 돕도록 하였습니다. - frontend/stagewise.json 파일을 삭제하였습니다.
This commit is contained in:
parent
6350fd6592
commit
f821a7bff3
|
|
@ -0,0 +1,729 @@
|
|||
# Flow 기반 반응형 레이아웃 설계서
|
||||
|
||||
> 작성일: 2026-01-30
|
||||
> 목표: 진정한 반응형 구현 (PC/태블릿/모바일 전체 대응)
|
||||
|
||||
---
|
||||
|
||||
## 1. 핵심 결론
|
||||
|
||||
### 1.1 현재 방식 vs 반응형 표준
|
||||
|
||||
| 항목 | 현재 시스템 | 웹 표준 (2025) |
|
||||
|------|-------------|----------------|
|
||||
| 배치 방식 | `position: absolute` | **Flexbox / CSS Grid** |
|
||||
| 좌표 | 픽셀 고정 (x, y) | **Flow 기반 (순서)** |
|
||||
| 화면 축소 시 | 그대로 (잘림) | **자동 재배치** |
|
||||
| 용도 | 툴팁, 오버레이 | **전체 레이아웃** |
|
||||
|
||||
> **결론**: `position: absolute`는 전체 레이아웃에 사용하면 안 됨 (웹 표준)
|
||||
|
||||
### 1.2 구현 방향
|
||||
|
||||
```
|
||||
절대 좌표 (x, y 픽셀)
|
||||
↓ 변환
|
||||
Flow 기반 배치 (Flexbox + Grid)
|
||||
↓ 결과
|
||||
화면 크기에 따라 자동 재배치
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 실제 화면 데이터 분석
|
||||
|
||||
### 2.1 분석 대상
|
||||
|
||||
```
|
||||
총 레이아웃: 1,250개
|
||||
총 컴포넌트: 5,236개
|
||||
분석 샘플: 6개 화면 (23, 20, 18, 16, 18, 5개 컴포넌트)
|
||||
```
|
||||
|
||||
### 2.2 화면 68 (수주 목록) - 가로 배치 패턴
|
||||
|
||||
```
|
||||
y=88: [분리] [저장] [수정] [삭제] ← 같은 행에 버튼 4개
|
||||
x=1277 x=1436 x=1594 x=1753
|
||||
|
||||
y=128: [────────── 테이블 ──────────]
|
||||
x=8, width=1904
|
||||
```
|
||||
|
||||
**변환 후**:
|
||||
```html
|
||||
<div class="flex flex-wrap justify-end gap-2"> <!-- Row 1 -->
|
||||
<button>분리</button>
|
||||
<button>저장</button>
|
||||
<button>수정</button>
|
||||
<button>삭제</button>
|
||||
</div>
|
||||
<div class="w-full"> <!-- Row 2 -->
|
||||
<Table />
|
||||
</div>
|
||||
```
|
||||
|
||||
**반응형 동작**:
|
||||
```
|
||||
1920px: [분리] [저장] [수정] [삭제] ← 가로 배치
|
||||
1280px: [분리] [저장] [수정] [삭제] ← 가로 배치 (공간 충분)
|
||||
768px: [분리] [저장] ← 줄바꿈 발생
|
||||
[수정] [삭제]
|
||||
375px: [분리] ← 세로 배치
|
||||
[저장]
|
||||
[수정]
|
||||
[삭제]
|
||||
```
|
||||
|
||||
### 2.3 화면 119 (장치 관리) - 2열 폼 패턴
|
||||
|
||||
```
|
||||
y=80: [장치 코드 ] [시리얼넘버 ]
|
||||
x=136, w=256 x=408, w=256
|
||||
|
||||
y=160: [제조사 ]
|
||||
x=136, w=528
|
||||
|
||||
y=240: [품번 ] [모델명 ]
|
||||
x=136, w=256 x=408, w=256
|
||||
|
||||
y=320: [구매일 ] [상태 ]
|
||||
y=400: [공급사 ] [구매 가격 ]
|
||||
y=480: [계약 번호 ] [공급사 전화 ]
|
||||
... (2열 반복)
|
||||
|
||||
y=840: [저장]
|
||||
x=544
|
||||
```
|
||||
|
||||
**변환 후**:
|
||||
```html
|
||||
<div class="grid grid-cols-2 gap-4"> <!-- 2열 그리드 -->
|
||||
<Input label="장치 코드" />
|
||||
<Input label="시리얼넘버" />
|
||||
</div>
|
||||
<div class="w-full"> <!-- 전체 너비 -->
|
||||
<Input label="제조사" />
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<Input label="품번" />
|
||||
<Select label="모델명" />
|
||||
</div>
|
||||
<!-- ... 반복 ... -->
|
||||
<div class="flex justify-center">
|
||||
<Button>저장</Button>
|
||||
</div>
|
||||
```
|
||||
|
||||
**반응형 동작**:
|
||||
```
|
||||
1920px: [장치 코드] [시리얼넘버] ← 2열
|
||||
1280px: [장치 코드] [시리얼넘버] ← 2열
|
||||
768px: [장치 코드] ← 1열
|
||||
[시리얼넘버]
|
||||
375px: [장치 코드] ← 1열
|
||||
[시리얼넘버]
|
||||
```
|
||||
|
||||
### 2.4 화면 4103 (수주 등록) - 섹션 기반 패턴
|
||||
|
||||
```
|
||||
y=20: [섹션: 옵션 설정 ]
|
||||
y=35: [입력방식▼] [판매유형▼] [단가방식▼] [☑ 단가수정]
|
||||
|
||||
y=110: [섹션: 거래처 정보 ]
|
||||
y=190: [거래처 * ] [담당자 ] [납품처 ] [납품장소 ]
|
||||
|
||||
y=260: [섹션: 추가된 품목 ]
|
||||
y=360: [리피터 테이블 ]
|
||||
|
||||
y=570: [섹션: 무역 정보 ]
|
||||
y=690: [인코텀즈▼] [결제조건▼] [통화▼ ]
|
||||
y=740: [선적항 ] [도착항 ] [HS Code ]
|
||||
|
||||
y=890: [섹션: 추가 정보 ]
|
||||
y=935: [메모 ]
|
||||
|
||||
y=1080: [저장]
|
||||
```
|
||||
|
||||
**변환 후**:
|
||||
```html
|
||||
<Card title="옵션 설정">
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<Select label="입력방식" />
|
||||
<Select label="판매유형" />
|
||||
<Select label="단가방식" />
|
||||
<Checkbox label="단가수정 허용" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card title="거래처 정보">
|
||||
<div class="grid grid-cols-4 gap-4"> <!-- 4열 그리드 -->
|
||||
<Select label="거래처 *" />
|
||||
<Input label="담당자" />
|
||||
<Input label="납품처" />
|
||||
<Input label="납품장소" class="col-span-2" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<!-- ... 섹션 반복 ... -->
|
||||
|
||||
<div class="flex justify-end">
|
||||
<Button>저장</Button>
|
||||
</div>
|
||||
```
|
||||
|
||||
**반응형 동작**:
|
||||
```
|
||||
1920px: [입력방식] [판매유형] [단가방식] [단가수정] ← 4열
|
||||
1280px: [입력방식] [판매유형] [단가방식] ← 3열
|
||||
[단가수정]
|
||||
768px: [입력방식] [판매유형] ← 2열
|
||||
[단가방식] [단가수정]
|
||||
375px: [입력방식] ← 1열
|
||||
[판매유형]
|
||||
[단가방식]
|
||||
[단가수정]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 변환 규칙
|
||||
|
||||
### 3.1 Row 그룹화 알고리즘
|
||||
|
||||
```typescript
|
||||
const ROW_THRESHOLD = 40; // px
|
||||
|
||||
function groupByRows(components: Component[]): Row[] {
|
||||
// 1. y 좌표로 정렬
|
||||
const sorted = [...components].sort((a, b) => a.position.y - b.position.y);
|
||||
|
||||
const rows: Row[] = [];
|
||||
let currentRow: Component[] = [];
|
||||
let currentY = -Infinity;
|
||||
|
||||
for (const comp of sorted) {
|
||||
if (comp.position.y - currentY > ROW_THRESHOLD) {
|
||||
// 새로운 Row 시작
|
||||
if (currentRow.length > 0) {
|
||||
rows.push({
|
||||
y: currentY,
|
||||
components: currentRow.sort((a, b) => a.position.x - b.position.x)
|
||||
});
|
||||
}
|
||||
currentRow = [comp];
|
||||
currentY = comp.position.y;
|
||||
} else {
|
||||
// 같은 Row에 추가
|
||||
currentRow.push(comp);
|
||||
}
|
||||
}
|
||||
|
||||
// 마지막 Row 추가
|
||||
if (currentRow.length > 0) {
|
||||
rows.push({
|
||||
y: currentY,
|
||||
components: currentRow.sort((a, b) => a.position.x - b.position.x)
|
||||
});
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 화면 68 적용 예시
|
||||
|
||||
**입력**:
|
||||
```json
|
||||
[
|
||||
{ "id": "comp_1899", "position": { "x": 1277, "y": 88 }, "text": "분리" },
|
||||
{ "id": "comp_1898", "position": { "x": 1436, "y": 88 }, "text": "저장" },
|
||||
{ "id": "comp_1897", "position": { "x": 1594, "y": 88 }, "text": "수정" },
|
||||
{ "id": "comp_1896", "position": { "x": 1753, "y": 88 }, "text": "삭제" },
|
||||
{ "id": "comp_1895", "position": { "x": 8, "y": 128 }, "type": "table" }
|
||||
]
|
||||
```
|
||||
|
||||
**변환 결과**:
|
||||
```json
|
||||
{
|
||||
"rows": [
|
||||
{
|
||||
"y": 88,
|
||||
"justify": "end",
|
||||
"components": ["comp_1899", "comp_1898", "comp_1897", "comp_1896"]
|
||||
},
|
||||
{
|
||||
"y": 128,
|
||||
"justify": "start",
|
||||
"components": ["comp_1895"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 정렬 방향 결정
|
||||
|
||||
```typescript
|
||||
function determineJustify(row: Row, screenWidth: number): string {
|
||||
const firstX = row.components[0].position.x;
|
||||
const lastComp = row.components[row.components.length - 1];
|
||||
const lastEnd = lastComp.position.x + lastComp.size.width;
|
||||
|
||||
// 왼쪽 여백 vs 오른쪽 여백 비교
|
||||
const leftMargin = firstX;
|
||||
const rightMargin = screenWidth - lastEnd;
|
||||
|
||||
if (leftMargin > rightMargin * 2) {
|
||||
return "end"; // 오른쪽 정렬
|
||||
} else if (rightMargin > leftMargin * 2) {
|
||||
return "start"; // 왼쪽 정렬
|
||||
} else {
|
||||
return "center"; // 중앙 정렬
|
||||
}
|
||||
}
|
||||
|
||||
// 화면 68 버튼 그룹:
|
||||
// leftMargin = 1277, rightMargin = 1920 - 1912 = 8
|
||||
// → "end" (오른쪽 정렬)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 렌더링 구현
|
||||
|
||||
### 4.1 새로운 FlowLayout 컴포넌트
|
||||
|
||||
```tsx
|
||||
// frontend/lib/registry/layouts/flow/FlowLayout.tsx
|
||||
|
||||
interface FlowLayoutProps {
|
||||
layout: LayoutData;
|
||||
renderer: DynamicComponentRenderer;
|
||||
}
|
||||
|
||||
export function FlowLayout({ layout, renderer }: FlowLayoutProps) {
|
||||
// 1. Row 그룹화
|
||||
const rows = useMemo(() => {
|
||||
return groupByRows(layout.components);
|
||||
}, [layout.components]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
{rows.map((row, index) => (
|
||||
<FlowRow
|
||||
key={index}
|
||||
row={row}
|
||||
renderer={renderer}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FlowRow({ row, renderer }: { row: Row; renderer: any }) {
|
||||
const justify = determineJustify(row, 1920);
|
||||
|
||||
const justifyClass = {
|
||||
start: "justify-start",
|
||||
center: "justify-center",
|
||||
end: "justify-end",
|
||||
}[justify];
|
||||
|
||||
return (
|
||||
<div className={`flex flex-wrap gap-2 ${justifyClass}`}>
|
||||
{row.components.map((comp) => (
|
||||
<div
|
||||
key={comp.id}
|
||||
style={{
|
||||
minWidth: comp.size.width,
|
||||
// width는 고정하지 않음 (flex로 자동 조정)
|
||||
}}
|
||||
>
|
||||
{renderer.renderChild(comp)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 기존 코드 수정 위치
|
||||
|
||||
**현재 (RealtimePreviewDynamic.tsx 라인 524-536)**:
|
||||
```tsx
|
||||
const baseStyle = {
|
||||
left: `${adjustedPositionX}px`, // ❌ 절대 좌표
|
||||
top: `${position.y}px`, // ❌ 절대 좌표
|
||||
position: "absolute", // ❌ 절대 위치
|
||||
};
|
||||
```
|
||||
|
||||
**변경 후**:
|
||||
```tsx
|
||||
// FlowLayout 사용 시 position 관련 스타일 제거
|
||||
const baseStyle = isFlowMode ? {
|
||||
// position, left, top 없음
|
||||
minWidth: size.width,
|
||||
height: size.height,
|
||||
} : {
|
||||
left: `${adjustedPositionX}px`,
|
||||
top: `${position.y}px`,
|
||||
position: "absolute",
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 가상 시뮬레이션
|
||||
|
||||
### 5.1 시나리오 1: 화면 68 (버튼 4개 + 테이블)
|
||||
|
||||
**렌더링 결과 (1920px)**:
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────────────┐
|
||||
│ [분리] [저장] [수정] [삭제] │
|
||||
│ flex-wrap, justify-end │
|
||||
├────────────────────────────────────────────────────────────────────────┤
|
||||
│ ┌────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 테이블 (w-full) │ │
|
||||
│ └────────────────────────────────────────────────────────────────────┘ │
|
||||
└────────────────────────────────────────────────────────────────────────┘
|
||||
✅ 정상: 버튼 오른쪽 정렬, 테이블 전체 너비
|
||||
```
|
||||
|
||||
**렌더링 결과 (1280px)**:
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ [분리] [저장] [수정] [삭제] │
|
||||
│ flex-wrap, justify-end │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ ┌─────────────────────────────────────────┐ │
|
||||
│ │ 테이블 (w-full) │ │
|
||||
│ └─────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────┘
|
||||
✅ 정상: 버튼 크기 유지, 테이블 너비 조정
|
||||
```
|
||||
|
||||
**렌더링 결과 (768px)**:
|
||||
```
|
||||
┌──────────────────────────┐
|
||||
│ [분리] [저장] │
|
||||
│ [수정] [삭제] │ ← 자동 줄바꿈!
|
||||
├──────────────────────────┤
|
||||
│ ┌──────────────────────┐ │
|
||||
│ │ 테이블 (w-full) │ │
|
||||
│ └──────────────────────┘ │
|
||||
└──────────────────────────┘
|
||||
✅ 정상: 버튼 줄바꿈, 테이블 너비 조정
|
||||
```
|
||||
|
||||
**렌더링 결과 (375px)**:
|
||||
```
|
||||
┌─────────────┐
|
||||
│ [분리] │
|
||||
│ [저장] │
|
||||
│ [수정] │
|
||||
│ [삭제] │ ← 세로 배치
|
||||
├─────────────┤
|
||||
│ ┌─────────┐ │
|
||||
│ │ 테이블 │ │ (가로 스크롤)
|
||||
│ └─────────┘ │
|
||||
└─────────────┘
|
||||
✅ 정상: 버튼 세로 배치, 테이블 가로 스크롤
|
||||
```
|
||||
|
||||
### 5.2 시나리오 2: 화면 119 (2열 폼)
|
||||
|
||||
**렌더링 결과 (1920px)**:
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────────────┐
|
||||
│ [장치 코드 ] [시리얼넘버 ] │
|
||||
│ grid-cols-2 │
|
||||
├────────────────────────────────────────────────────────────────────────┤
|
||||
│ [제조사 ] │
|
||||
│ col-span-2 (전체 너비) │
|
||||
├────────────────────────────────────────────────────────────────────────┤
|
||||
│ [품번 ] [모델명▼ ] │
|
||||
│ ... │
|
||||
└────────────────────────────────────────────────────────────────────────┘
|
||||
✅ 정상: 2열 그리드
|
||||
```
|
||||
|
||||
**렌더링 결과 (768px)**:
|
||||
```
|
||||
┌──────────────────────────┐
|
||||
│ [장치 코드 ] │
|
||||
│ [시리얼넘버 ] │ ← 1열로 변경
|
||||
├──────────────────────────┤
|
||||
│ [제조사 ] │
|
||||
├──────────────────────────┤
|
||||
│ [품번 ] │
|
||||
│ [모델명▼ ] │
|
||||
│ ... │
|
||||
└──────────────────────────┘
|
||||
✅ 정상: 1열 그리드
|
||||
```
|
||||
|
||||
### 5.3 시나리오 3: 분할 패널
|
||||
|
||||
**현재 SplitPanelLayout 동작**:
|
||||
```
|
||||
좌측 60% | 우측 40% ← 이미 퍼센트 기반
|
||||
```
|
||||
|
||||
**변경 후 (768px 이하)**:
|
||||
```
|
||||
┌────────────────────┐
|
||||
│ 좌측 100% │
|
||||
├────────────────────┤
|
||||
│ 우측 100% │
|
||||
└────────────────────┘
|
||||
← 세로 배치로 전환
|
||||
```
|
||||
|
||||
**구현**:
|
||||
```tsx
|
||||
// SplitPanelLayoutComponent.tsx
|
||||
const isMobile = useMediaQuery("(max-width: 768px)");
|
||||
|
||||
return (
|
||||
<div className={isMobile ? "flex-col" : "flex-row"}>
|
||||
<div className={isMobile ? "w-full" : "w-[60%]"}>
|
||||
{/* 좌측 패널 */}
|
||||
</div>
|
||||
<div className={isMobile ? "w-full" : "w-[40%]"}>
|
||||
{/* 우측 패널 */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 엣지 케이스 검증
|
||||
|
||||
### 6.1 겹치는 컴포넌트
|
||||
|
||||
**현재 데이터 (화면 74)**:
|
||||
```json
|
||||
{ "id": "comp_2606", "position": { "x": 161, "y": 400 } }, // 분할 패널
|
||||
{ "id": "comp_fkk75q08", "position": { "x": 161, "y": 400 } } // 라디오 버튼
|
||||
```
|
||||
|
||||
**문제**: 같은 위치에 두 컴포넌트 → z-index로 겹쳐서 표시
|
||||
|
||||
**해결**:
|
||||
- z-index가 높은 컴포넌트 우선
|
||||
- 또는 parent-child 관계면 중첩 처리
|
||||
|
||||
```typescript
|
||||
function resolveOverlaps(row: Row): Row {
|
||||
// z-index로 정렬하여 높은 것만 표시
|
||||
// 또는 parentId 확인하여 중첩 처리
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 조건부 표시 컴포넌트
|
||||
|
||||
**현재 데이터 (화면 4103)**:
|
||||
```json
|
||||
{
|
||||
"id": "section-customer-info",
|
||||
"conditionalConfig": {
|
||||
"field": "input_method",
|
||||
"value": "customer_first",
|
||||
"action": "show"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**동작**: 조건에 따라 show/hide
|
||||
**Flow 레이아웃에서**: 숨겨지면 공간도 사라짐 (flex 자동 조정)
|
||||
|
||||
✅ 문제없음
|
||||
|
||||
### 6.3 테이블 + 버튼 조합
|
||||
|
||||
**패턴**:
|
||||
```
|
||||
[버튼 그룹] ← flex-wrap, justify-end
|
||||
[테이블] ← w-full
|
||||
```
|
||||
|
||||
**테이블 가로 스크롤**:
|
||||
- 테이블 내부는 가로 스크롤 지원
|
||||
- 외부 컨테이너는 w-full
|
||||
|
||||
✅ 문제없음
|
||||
|
||||
### 6.4 섹션 카드 내부 컴포넌트
|
||||
|
||||
**현재**: 섹션 카드와 내부 컴포넌트가 별도로 저장됨
|
||||
|
||||
**변환 시**:
|
||||
1. 섹션 카드의 y 범위 파악
|
||||
2. 해당 y 범위 내 컴포넌트들을 섹션 자식으로 그룹화
|
||||
3. 섹션 내부에서 다시 Row 그룹화
|
||||
|
||||
```typescript
|
||||
function groupWithinSection(
|
||||
section: Component,
|
||||
allComponents: Component[]
|
||||
): Component[] {
|
||||
const sectionTop = section.position.y;
|
||||
const sectionBottom = section.position.y + section.size.height;
|
||||
|
||||
return allComponents.filter(comp => {
|
||||
return comp.id !== section.id &&
|
||||
comp.position.y >= sectionTop &&
|
||||
comp.position.y < sectionBottom;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 호환성 검증
|
||||
|
||||
### 7.1 기존 기능 호환
|
||||
|
||||
| 기능 | 호환 여부 | 설명 |
|
||||
|------|----------|------|
|
||||
| 디자인 모드 | ⚠️ 수정 필요 | 드래그 앤 드롭 로직 수정 |
|
||||
| 미리보기 | ✅ 호환 | Flow 레이아웃으로 렌더링 |
|
||||
| 조건부 표시 | ✅ 호환 | flex로 자동 조정 |
|
||||
| 분할 패널 | ⚠️ 수정 필요 | 반응형 전환 로직 추가 |
|
||||
| 테이블 | ✅ 호환 | w-full 적용 |
|
||||
| 모달 | ✅ 호환 | 모달 내부도 Flow 적용 |
|
||||
|
||||
### 7.2 디자인 모드 수정
|
||||
|
||||
**현재**: 드래그하면 x, y 픽셀 저장
|
||||
**변경 후**: 드래그하면 x, y 픽셀 저장 (동일) → 렌더링 시 변환
|
||||
|
||||
```
|
||||
저장: 픽셀 좌표 (기존 유지)
|
||||
렌더링: Flow 기반으로 변환
|
||||
```
|
||||
|
||||
**장점**: DB 마이그레이션 불필요
|
||||
|
||||
---
|
||||
|
||||
## 8. 구현 계획
|
||||
|
||||
### Phase 1: 핵심 변환 로직 (1일)
|
||||
|
||||
1. `groupByRows()` 함수 구현
|
||||
2. `determineJustify()` 함수 구현
|
||||
3. `FlowLayout` 컴포넌트 생성
|
||||
|
||||
### Phase 2: 렌더링 적용 (1일)
|
||||
|
||||
1. `DynamicComponentRenderer`에 Flow 모드 추가
|
||||
2. `RealtimePreviewDynamic` 수정
|
||||
3. 기존 absolute 스타일 조건부 적용
|
||||
|
||||
### Phase 3: 특수 케이스 처리 (1일)
|
||||
|
||||
1. 섹션 카드 내부 그룹화
|
||||
2. 겹치는 컴포넌트 처리
|
||||
3. 분할 패널 반응형 전환
|
||||
|
||||
### Phase 4: 테스트 (1일)
|
||||
|
||||
1. 화면 68 (버튼 + 테이블) 테스트
|
||||
2. 화면 119 (2열 폼) 테스트
|
||||
3. 화면 4103 (복잡한 폼) 테스트
|
||||
4. PC 1920px → 1280px 테스트
|
||||
5. 태블릿 768px 테스트
|
||||
6. 모바일 375px 테스트
|
||||
|
||||
---
|
||||
|
||||
## 9. 예상 이슈
|
||||
|
||||
### 9.1 디자이너 의도 손실
|
||||
|
||||
**문제**: 디자이너가 의도적으로 배치한 위치가 변경될 수 있음
|
||||
|
||||
**해결**:
|
||||
- 기본 Flow 레이아웃 적용
|
||||
- 필요시 `flexOrder` 속성으로 순서 조정 가능
|
||||
- 또는 `fixedPosition: true` 옵션으로 절대 좌표 유지
|
||||
|
||||
### 9.2 복잡한 레이아웃
|
||||
|
||||
**문제**: 일부 화면은 자유 배치가 필요할 수 있음
|
||||
|
||||
**해결**:
|
||||
- 화면별 `layoutMode` 설정
|
||||
- `"flow"`: Flow 기반 (기본값)
|
||||
- `"absolute"`: 기존 절대 좌표
|
||||
|
||||
### 9.3 성능
|
||||
|
||||
**문제**: 매 렌더링마다 Row 그룹화 계산
|
||||
|
||||
**해결**:
|
||||
- `useMemo`로 캐싱
|
||||
- 컴포넌트 목록 변경 시에만 재계산
|
||||
|
||||
---
|
||||
|
||||
## 10. 최종 체크리스트
|
||||
|
||||
### 구현 전
|
||||
|
||||
- [ ] 현재 동작하는 화면 스크린샷 (비교용)
|
||||
- [ ] 테스트 화면 목록 확정 (68, 119, 4103)
|
||||
|
||||
### 구현 중
|
||||
|
||||
- [ ] `groupByRows()` 구현
|
||||
- [ ] `determineJustify()` 구현
|
||||
- [ ] `FlowLayout` 컴포넌트 생성
|
||||
- [ ] `DynamicComponentRenderer` 수정
|
||||
- [ ] `RealtimePreviewDynamic` 수정
|
||||
|
||||
### 테스트
|
||||
|
||||
- [ ] 1920px 테스트
|
||||
- [ ] 1280px 테스트
|
||||
- [ ] 768px 테스트
|
||||
- [ ] 375px 테스트
|
||||
- [ ] 디자인 모드 테스트
|
||||
- [ ] 분할 패널 테스트
|
||||
- [ ] 조건부 표시 테스트
|
||||
|
||||
---
|
||||
|
||||
## 11. 결론
|
||||
|
||||
### 11.1 구현 가능 여부
|
||||
|
||||
**✅ 가능**
|
||||
|
||||
- 기존 데이터 구조 유지 (DB 변경 없음)
|
||||
- 렌더링 레벨에서만 변환
|
||||
- 모든 화면 패턴 분석 완료
|
||||
- 엣지 케이스 해결책 확보
|
||||
|
||||
### 11.2 핵심 변경 사항
|
||||
|
||||
```
|
||||
Before: position: absolute + left/top 픽셀
|
||||
After: Flexbox + flex-wrap + justify-*
|
||||
```
|
||||
|
||||
### 11.3 예상 효과
|
||||
|
||||
| 화면 크기 | Before | After |
|
||||
|-----------|--------|-------|
|
||||
| 1920px | 정상 | 정상 |
|
||||
| 1280px | 버튼 잘림 | **자동 조정** |
|
||||
| 768px | 레이아웃 깨짐 | **자동 재배치** |
|
||||
| 375px | 사용 불가 | **자동 세로 배치** |
|
||||
|
|
@ -0,0 +1,688 @@
|
|||
# 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 반응형을 구현할 수 있습니다.
|
||||
|
|
@ -0,0 +1,325 @@
|
|||
# 본서버 → 개발서버 마이그레이션 가이드 (공용)
|
||||
|
||||
> **이 문서는 다음 AI 에이전트가 마이그레이션 작업을 이어받을 때 참고하는 핵심 가이드입니다.**
|
||||
|
||||
---
|
||||
|
||||
## 빠른 시작
|
||||
|
||||
### 마이그레이션 방향 (절대 잊지 말 것)
|
||||
|
||||
```
|
||||
본서버 (Production) → 개발서버 (Development)
|
||||
211.115.91.141:11134 39.117.244.52:11132
|
||||
screen_layouts (V1) screen_layouts_v2 (V2)
|
||||
```
|
||||
|
||||
**반대로 하면 안 됨!** 개발서버 완성 후 → 본서버로 배포 예정
|
||||
|
||||
### DB 접속 정보
|
||||
|
||||
```bash
|
||||
# 본서버 (Production)
|
||||
docker exec pms-backend-mac node -e '
|
||||
const { Pool } = require("pg");
|
||||
const pool = new Pool({
|
||||
connectionString: "postgresql://postgres:vexplor0909!!@211.115.91.141:11134/plm?sslmode=disable",
|
||||
ssl: false
|
||||
});
|
||||
// 쿼리 실행
|
||||
'
|
||||
|
||||
# 개발서버 (Development)
|
||||
docker exec pms-backend-mac node -e '
|
||||
const { Pool } = require("pg");
|
||||
const pool = new Pool({
|
||||
connectionString: "postgresql://postgres:ph0909!!@39.117.244.52:11132/plm?sslmode=disable",
|
||||
ssl: false
|
||||
});
|
||||
// 쿼리 실행
|
||||
'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 핵심 개념
|
||||
|
||||
### V1 vs V2 구조 차이
|
||||
|
||||
| 구분 | V1 (본서버) | V2 (개발서버) |
|
||||
|------|-------------|---------------|
|
||||
| 테이블 | screen_layouts | screen_layouts_v2 |
|
||||
| 레코드 | 컴포넌트별 1개 | 화면당 1개 |
|
||||
| 설정 저장 | properties JSONB | layout_data.components[].overrides |
|
||||
| 채번/카테고리 | menu_objid 기반 | table_name + column_name 기반 |
|
||||
| 컴포넌트 참조 | component_type 문자열 | url 경로 (@/lib/registry/...) |
|
||||
|
||||
### 데이터 타입 관리 (V2)
|
||||
|
||||
```
|
||||
table_type_columns (input_type)
|
||||
├── 'category' → category_values 테이블
|
||||
├── 'numbering' → numbering_rules 테이블 (detail_settings.numberingRuleId)
|
||||
├── 'entity' → 엔티티 검색
|
||||
└── 'text', 'number', 'date', etc.
|
||||
```
|
||||
|
||||
### 컴포넌트 URL 매핑
|
||||
|
||||
```typescript
|
||||
const V1_TO_V2_MAPPING = {
|
||||
'table-list': '@/lib/registry/components/v2-table-list',
|
||||
'button-primary': '@/lib/registry/components/v2-button-primary',
|
||||
'text-input': '@/lib/registry/components/v2-text-input',
|
||||
'select-basic': '@/lib/registry/components/v2-select',
|
||||
'date-input': '@/lib/registry/components/v2-date-input',
|
||||
'entity-search-input': '@/lib/registry/components/v2-entity-search',
|
||||
'category-manager': '@/lib/registry/components/v2-category-manager',
|
||||
'numbering-rule': '@/lib/registry/components/v2-numbering-rule',
|
||||
'tabs-widget': '@/lib/registry/components/v2-tabs-widget',
|
||||
'textarea-basic': '@/lib/registry/components/v2-textarea',
|
||||
};
|
||||
```
|
||||
|
||||
### 모달 처리 방식 변경
|
||||
|
||||
- **V1**: 별도 화면(screen_id)으로 모달 관리
|
||||
- **V2**: 부모 화면에 overlay/dialog 컴포넌트로 통합
|
||||
|
||||
---
|
||||
|
||||
## 마이그레이션 대상 메뉴 현황
|
||||
|
||||
### 품질관리 (우선순위 1)
|
||||
|
||||
| 본서버 코드 | 화면명 | 상태 | 비고 |
|
||||
|-------------|--------|------|------|
|
||||
| COMPANY_7_126 | 검사정보 관리 | ✅ V2 존재 | 컴포넌트 검증 필요 |
|
||||
| COMPANY_7_127 | 품목옵션 설정 | ✅ V2 존재 | v2-category-manager 사용중 |
|
||||
| COMPANY_7_138 | 카테고리 설정 | ❌ 누락 | table_name 기반으로 변경 |
|
||||
| COMPANY_7_139 | 코드 설정 | ❌ 누락 | table_name 기반으로 변경 |
|
||||
| COMPANY_7_142 | 검사장비 관리 | ❌ 누락 | 모달 통합 필요 |
|
||||
| COMPANY_7_143 | 검사장비 등록모달 | ❌ 누락 | → 142에 통합 |
|
||||
| COMPANY_7_144 | 불량기준 정보 | ❌ 누락 | 모달 통합 필요 |
|
||||
| COMPANY_7_145 | 불량기준 등록모달 | ❌ 누락 | → 144에 통합 |
|
||||
|
||||
### 다음 마이그레이션 대상 (미정)
|
||||
|
||||
- [ ] 물류관리
|
||||
- [ ] 생산관리
|
||||
- [ ] 영업관리
|
||||
- [ ] 기타 메뉴들
|
||||
|
||||
---
|
||||
|
||||
## 마이그레이션 작업 절차
|
||||
|
||||
### Step 1: 분석
|
||||
|
||||
```sql
|
||||
-- 본서버 특정 메뉴 화면 목록 조회
|
||||
SELECT
|
||||
sd.screen_id, sd.screen_code, sd.screen_name, sd.table_name,
|
||||
COUNT(sl.layout_id) as component_count
|
||||
FROM screen_definitions sd
|
||||
LEFT JOIN screen_layouts sl ON sd.screen_id = sl.screen_id
|
||||
WHERE sd.screen_name LIKE '%[메뉴명]%'
|
||||
AND sd.company_code = 'COMPANY_7'
|
||||
GROUP BY sd.screen_id, sd.screen_code, sd.screen_name, sd.table_name;
|
||||
|
||||
-- 개발서버 V2 현황 확인
|
||||
SELECT
|
||||
sd.screen_id, sd.screen_code, sd.screen_name,
|
||||
sv2.layout_id IS NOT NULL as has_v2
|
||||
FROM screen_definitions sd
|
||||
LEFT JOIN screen_layouts_v2 sv2 ON sd.screen_id = sv2.screen_id
|
||||
WHERE sd.company_code = 'COMPANY_7';
|
||||
```
|
||||
|
||||
### Step 2: screen_definitions 동기화
|
||||
|
||||
본서버에만 있는 화면을 개발서버에 추가
|
||||
|
||||
### Step 3: V1 → V2 레이아웃 변환
|
||||
|
||||
```typescript
|
||||
// layout_data 구조
|
||||
{
|
||||
"version": "2.0",
|
||||
"components": [
|
||||
{
|
||||
"id": "comp_xxx",
|
||||
"url": "@/lib/registry/components/v2-table-list",
|
||||
"position": { "x": 0, "y": 0 },
|
||||
"size": { "width": 100, "height": 50 },
|
||||
"displayOrder": 0,
|
||||
"overrides": {
|
||||
"tableName": "테이블명",
|
||||
"columns": ["컬럼1", "컬럼2"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: 카테고리 데이터 확인/생성
|
||||
|
||||
```sql
|
||||
-- 테이블의 category 컬럼 확인
|
||||
SELECT column_name, column_label
|
||||
FROM table_type_columns
|
||||
WHERE table_name = '[테이블명]'
|
||||
AND input_type = 'category';
|
||||
|
||||
-- category_values 데이터 확인
|
||||
SELECT value_id, value_code, value_label
|
||||
FROM category_values
|
||||
WHERE table_name = '[테이블명]'
|
||||
AND column_name = '[컬럼명]'
|
||||
AND company_code = 'COMPANY_7';
|
||||
```
|
||||
|
||||
### Step 5: 채번 규칙 확인/생성
|
||||
|
||||
```sql
|
||||
-- numbering 컬럼 확인
|
||||
SELECT column_name, column_label, detail_settings
|
||||
FROM table_type_columns
|
||||
WHERE table_name = '[테이블명]'
|
||||
AND input_type = 'numbering';
|
||||
|
||||
-- numbering_rules 데이터 확인
|
||||
SELECT rule_id, rule_name, table_name, column_name
|
||||
FROM numbering_rules
|
||||
WHERE company_code = 'COMPANY_7';
|
||||
```
|
||||
|
||||
### Step 6: 검증
|
||||
|
||||
- [ ] 화면 렌더링 확인
|
||||
- [ ] 컴포넌트 동작 확인
|
||||
- [ ] 저장/수정/삭제 테스트
|
||||
- [ ] 카테고리 드롭다운 동작
|
||||
- [ ] 채번 규칙 동작
|
||||
|
||||
---
|
||||
|
||||
## 핵심 테이블 스키마
|
||||
|
||||
### screen_layouts_v2
|
||||
|
||||
```sql
|
||||
CREATE TABLE screen_layouts_v2 (
|
||||
layout_id SERIAL PRIMARY KEY,
|
||||
screen_id INTEGER NOT NULL,
|
||||
company_code VARCHAR(20) NOT NULL,
|
||||
layout_data JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(screen_id, company_code)
|
||||
);
|
||||
```
|
||||
|
||||
### category_values
|
||||
|
||||
```sql
|
||||
-- 핵심 컬럼
|
||||
value_id, table_name, column_name, value_code, value_label,
|
||||
parent_value_id, depth, path, company_code
|
||||
```
|
||||
|
||||
### numbering_rules + numbering_rule_parts
|
||||
|
||||
```sql
|
||||
-- numbering_rules 핵심 컬럼
|
||||
rule_id, rule_name, table_name, column_name, separator,
|
||||
reset_period, current_sequence, company_code
|
||||
|
||||
-- numbering_rule_parts 핵심 컬럼
|
||||
rule_id, part_order, part_type, generation_method,
|
||||
auto_config, manual_config, company_code
|
||||
```
|
||||
|
||||
### table_type_columns
|
||||
|
||||
```sql
|
||||
-- 핵심 컬럼
|
||||
table_name, column_name, input_type, column_label,
|
||||
detail_settings, company_code
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 참고 문서
|
||||
|
||||
### 필수 읽기
|
||||
|
||||
1. **[본서버_개발서버_마이그레이션_상세가이드.md](./본서버_개발서버_마이그레이션_상세가이드.md)** - 상세 마이그레이션 절차
|
||||
2. **[화면개발_표준_가이드.md](../screen-implementation-guide/화면개발_표준_가이드.md)** - V2 화면 개발 표준
|
||||
3. **[SCREEN_DEVELOPMENT_STANDARD.md](../screen-implementation-guide/SCREEN_DEVELOPMENT_STANDARD.md)** - 영문 표준 가이드
|
||||
|
||||
### 코드 참조
|
||||
|
||||
| 파일 | 설명 |
|
||||
|------|------|
|
||||
| `backend-node/src/services/categoryTreeService.ts` | 카테고리 관리 서비스 |
|
||||
| `backend-node/src/services/numberingRuleService.ts` | 채번 규칙 서비스 |
|
||||
| `frontend/lib/registry/components/v2-category-manager/` | V2 카테고리 컴포넌트 |
|
||||
| `frontend/lib/registry/components/v2-numbering-rule/` | V2 채번 컴포넌트 |
|
||||
|
||||
### 관련 문서
|
||||
|
||||
- `docs/V2_컴포넌트_분석_가이드.md`
|
||||
- `docs/V2_컴포넌트_연동_가이드.md`
|
||||
- `docs/DDD1542/COMPONENT_LAYOUT_V2_ARCHITECTURE.md`
|
||||
- `docs/DDD1542/COMPONENT_MIGRATION_PLAN.md`
|
||||
|
||||
---
|
||||
|
||||
## 주의사항
|
||||
|
||||
### 절대 하지 말 것
|
||||
|
||||
1. **개발서버 → 본서버 마이그레이션** (반대 방향)
|
||||
2. **본서버 데이터 직접 수정** (SELECT만 허용)
|
||||
3. **company_code 누락** (멀티테넌시 필수)
|
||||
|
||||
### 반드시 할 것
|
||||
|
||||
1. 마이그레이션 전 **개발서버 백업**
|
||||
2. 컴포넌트 변환 시 **V2 컴포넌트만 사용** (v2- prefix)
|
||||
3. 모달 화면은 **부모 화면에 통합**
|
||||
4. 카테고리/채번은 **table_name + column_name 기반**
|
||||
|
||||
---
|
||||
|
||||
## 마이그레이션 진행 로그
|
||||
|
||||
| 날짜 | 메뉴 | 담당 | 상태 | 비고 |
|
||||
|------|------|------|------|------|
|
||||
| 2026-02-03 | 품질관리 | DDD1542 | 분석 완료 | 마이그레이션 대기 |
|
||||
| | 물류관리 | - | 미시작 | |
|
||||
| | 생산관리 | - | 미시작 | |
|
||||
| | 영업관리 | - | 미시작 | |
|
||||
|
||||
---
|
||||
|
||||
## 다음 작업 요청 예시
|
||||
|
||||
다음 AI에게 요청할 때 이렇게 말하면 됩니다:
|
||||
|
||||
```
|
||||
"본서버_개발서버_마이그레이션_가이드.md 읽고 품질관리 메뉴 마이그레이션 진행해줘"
|
||||
|
||||
"본서버_개발서버_마이그레이션_가이드.md 참고해서 물류관리 메뉴 분석해줘"
|
||||
|
||||
"본서버_개발서버_마이그레이션_상세가이드.md 보고 COMPANY_7_142 화면 V2로 변환해줘"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 변경 이력
|
||||
|
||||
| 날짜 | 작성자 | 내용 |
|
||||
|------|--------|------|
|
||||
| 2026-02-03 | DDD1542 | 초안 작성 |
|
||||
|
|
@ -0,0 +1,553 @@
|
|||
# 본서버 → 개발서버 마이그레이션 가이드
|
||||
|
||||
## 개요
|
||||
|
||||
본 문서는 **본서버(Production)**의 `screen_layouts` (V1) 데이터를 **개발서버(Development)**의 `screen_layouts_v2` 시스템으로 마이그레이션하는 절차를 정의합니다.
|
||||
|
||||
### 마이그레이션 방향
|
||||
```
|
||||
본서버 (Production) 개발서버 (Development)
|
||||
┌─────────────────────┐ ┌─────────────────────┐
|
||||
│ screen_layouts (V1) │ → │ screen_layouts_v2 │
|
||||
│ - 컴포넌트별 레코드 │ │ - 화면당 1개 레코드 │
|
||||
│ - properties JSONB │ │ - layout_data JSONB │
|
||||
└─────────────────────┘ └─────────────────────┘
|
||||
```
|
||||
|
||||
### 최종 목표
|
||||
개발서버에서 완성 후 **개발서버 → 본서버**로 배포
|
||||
|
||||
---
|
||||
|
||||
## 1. V1 vs V2 구조 차이
|
||||
|
||||
### 1.1 screen_layouts (V1) - 본서버
|
||||
|
||||
```sql
|
||||
-- 컴포넌트별 1개 레코드
|
||||
CREATE TABLE screen_layouts (
|
||||
layout_id SERIAL PRIMARY KEY,
|
||||
screen_id INTEGER,
|
||||
component_type VARCHAR(50),
|
||||
component_id VARCHAR(100),
|
||||
properties JSONB, -- 모든 설정값 포함
|
||||
...
|
||||
);
|
||||
```
|
||||
|
||||
**특징:**
|
||||
- 화면당 N개 레코드 (컴포넌트 수만큼)
|
||||
- `properties`에 모든 설정 저장 (defaults + overrides 구분 없음)
|
||||
- `menu_objid` 기반 채번/카테고리 관리
|
||||
|
||||
### 1.2 screen_layouts_v2 - 개발서버
|
||||
|
||||
```sql
|
||||
-- 화면당 1개 레코드
|
||||
CREATE TABLE screen_layouts_v2 (
|
||||
layout_id SERIAL PRIMARY KEY,
|
||||
screen_id INTEGER NOT NULL,
|
||||
company_code VARCHAR(20) NOT NULL,
|
||||
layout_data JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
UNIQUE(screen_id, company_code)
|
||||
);
|
||||
```
|
||||
|
||||
**layout_data 구조:**
|
||||
```json
|
||||
{
|
||||
"version": "2.0",
|
||||
"components": [
|
||||
{
|
||||
"id": "comp_xxx",
|
||||
"url": "@/lib/registry/components/v2-table-list",
|
||||
"position": { "x": 0, "y": 0 },
|
||||
"size": { "width": 100, "height": 50 },
|
||||
"displayOrder": 0,
|
||||
"overrides": {
|
||||
"tableName": "inspection_standard",
|
||||
"columns": ["id", "name"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"updatedAt": "2026-02-03T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**특징:**
|
||||
- 화면당 1개 레코드
|
||||
- `url` + `overrides` 방식 (Zod 스키마 defaults와 병합)
|
||||
- `table_name + column_name` 기반 채번/카테고리 관리 (전역)
|
||||
|
||||
---
|
||||
|
||||
## 2. 데이터 타입 관리 구조 (V2)
|
||||
|
||||
### 2.1 핵심 테이블 관계
|
||||
|
||||
```
|
||||
table_type_columns (컬럼 타입 정의)
|
||||
├── input_type = 'category' → category_values
|
||||
├── input_type = 'numbering' → numbering_rules
|
||||
└── input_type = 'text', 'date', 'number', etc.
|
||||
```
|
||||
|
||||
### 2.2 table_type_columns
|
||||
|
||||
각 테이블의 컬럼별 입력 타입을 정의합니다.
|
||||
|
||||
```sql
|
||||
SELECT table_name, column_name, input_type, column_label
|
||||
FROM table_type_columns
|
||||
WHERE input_type IN ('category', 'numbering');
|
||||
```
|
||||
|
||||
**주요 input_type:**
|
||||
| input_type | 설명 | 연결 테이블 |
|
||||
|------------|------|-------------|
|
||||
| text | 텍스트 입력 | - |
|
||||
| number | 숫자 입력 | - |
|
||||
| date | 날짜 입력 | - |
|
||||
| category | 카테고리 드롭다운 | category_values |
|
||||
| numbering | 자동 채번 | numbering_rules |
|
||||
| entity | 엔티티 검색 | - |
|
||||
|
||||
### 2.3 category_values (카테고리 관리)
|
||||
|
||||
```sql
|
||||
-- 카테고리 값 조회
|
||||
SELECT value_id, table_name, column_name, value_code, value_label,
|
||||
parent_value_id, depth, company_code
|
||||
FROM category_values
|
||||
WHERE table_name = 'inspection_standard'
|
||||
AND column_name = 'inspection_method'
|
||||
AND company_code = 'COMPANY_7';
|
||||
```
|
||||
|
||||
**V1 vs V2 차이:**
|
||||
| 구분 | V1 | V2 |
|
||||
|------|----|----|
|
||||
| 키 | menu_objid | table_name + column_name |
|
||||
| 범위 | 화면별 | 전역 (테이블.컬럼별) |
|
||||
| 계층 | 단일 | 3단계 (대/중/소분류) |
|
||||
|
||||
### 2.4 numbering_rules (채번 규칙)
|
||||
|
||||
```sql
|
||||
-- 채번 규칙 조회
|
||||
SELECT rule_id, rule_name, table_name, column_name, separator,
|
||||
reset_period, current_sequence, company_code
|
||||
FROM numbering_rules
|
||||
WHERE company_code = 'COMPANY_7';
|
||||
```
|
||||
|
||||
**연결 방식:**
|
||||
```
|
||||
table_type_columns.detail_settings = '{"numberingRuleId": "rule-xxx"}'
|
||||
↓
|
||||
numbering_rules.rule_id = "rule-xxx"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 컴포넌트 매핑
|
||||
|
||||
### 3.1 기본 컴포넌트 매핑
|
||||
|
||||
| V1 (본서버) | V2 (개발서버) | 비고 |
|
||||
|-------------|---------------|------|
|
||||
| table-list | v2-table-list | 테이블 목록 |
|
||||
| button-primary | v2-button-primary | 버튼 |
|
||||
| text-input | v2-text-input | 텍스트 입력 |
|
||||
| select-basic | v2-select | 드롭다운 |
|
||||
| date-input | v2-date-input | 날짜 입력 |
|
||||
| entity-search-input | v2-entity-search | 엔티티 검색 |
|
||||
| tabs-widget | v2-tabs-widget | 탭 |
|
||||
|
||||
### 3.2 특수 컴포넌트 매핑
|
||||
|
||||
| V1 (본서버) | V2 (개발서버) | 마이그레이션 방식 |
|
||||
|-------------|---------------|-------------------|
|
||||
| category-manager | v2-category-manager | table_name 기반으로 변경 |
|
||||
| numbering-rule | v2-numbering-rule | table_name 기반으로 변경 |
|
||||
| 모달 화면 | overlay 통합 | 부모 화면에 통합 |
|
||||
|
||||
### 3.3 모달 처리 방식 변경
|
||||
|
||||
**V1 (본서버):**
|
||||
```
|
||||
화면 A (screen_id: 142) - 검사장비관리
|
||||
└── 버튼 클릭 → 화면 B (screen_id: 143) - 검사장비 등록모달
|
||||
```
|
||||
|
||||
**V2 (개발서버):**
|
||||
```
|
||||
화면 A (screen_id: 142) - 검사장비관리
|
||||
└── v2-dialog-form 컴포넌트로 모달 통합
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 마이그레이션 절차
|
||||
|
||||
### 4.1 사전 분석
|
||||
|
||||
```sql
|
||||
-- 1. 본서버 화면 목록 확인
|
||||
SELECT sd.screen_id, sd.screen_code, sd.screen_name, sd.table_name,
|
||||
COUNT(sl.layout_id) as component_count
|
||||
FROM screen_definitions sd
|
||||
LEFT JOIN screen_layouts sl ON sd.screen_id = sl.screen_id
|
||||
WHERE sd.screen_code LIKE 'COMPANY_7_%'
|
||||
AND sd.screen_name LIKE '%품질%'
|
||||
GROUP BY sd.screen_id, sd.screen_code, sd.screen_name, sd.table_name;
|
||||
|
||||
-- 2. 개발서버 V2 화면 현황 확인
|
||||
SELECT sd.screen_id, sd.screen_code, sd.screen_name,
|
||||
sv2.layout_data IS NOT NULL as has_v2_layout
|
||||
FROM screen_definitions sd
|
||||
LEFT JOIN screen_layouts_v2 sv2 ON sd.screen_id = sv2.screen_id
|
||||
WHERE sd.company_code = 'COMPANY_7';
|
||||
```
|
||||
|
||||
### 4.2 Step 1: screen_definitions 동기화
|
||||
|
||||
```sql
|
||||
-- 본서버에만 있는 화면을 개발서버에 추가
|
||||
INSERT INTO screen_definitions (screen_code, screen_name, table_name, company_code, ...)
|
||||
SELECT screen_code, screen_name, table_name, company_code, ...
|
||||
FROM [본서버].screen_definitions
|
||||
WHERE screen_code NOT IN (SELECT screen_code FROM screen_definitions);
|
||||
```
|
||||
|
||||
### 4.3 Step 2: V1 → V2 레이아웃 변환
|
||||
|
||||
```typescript
|
||||
// 변환 로직 (pseudo-code)
|
||||
async function convertV1toV2(screenId: number, companyCode: string) {
|
||||
// 1. V1 레이아웃 조회
|
||||
const v1Layouts = await getV1Layouts(screenId);
|
||||
|
||||
// 2. V2 형식으로 변환
|
||||
const v2Layout = {
|
||||
version: "2.0",
|
||||
components: v1Layouts.map(v1 => ({
|
||||
id: v1.component_id,
|
||||
url: mapComponentUrl(v1.component_type),
|
||||
position: { x: v1.position_x, y: v1.position_y },
|
||||
size: { width: v1.width, height: v1.height },
|
||||
displayOrder: v1.display_order,
|
||||
overrides: extractOverrides(v1.properties)
|
||||
})),
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
// 3. V2 테이블에 저장
|
||||
await saveV2Layout(screenId, companyCode, v2Layout);
|
||||
}
|
||||
|
||||
function mapComponentUrl(v1Type: string): string {
|
||||
const mapping = {
|
||||
'table-list': '@/lib/registry/components/v2-table-list',
|
||||
'button-primary': '@/lib/registry/components/v2-button-primary',
|
||||
'category-manager': '@/lib/registry/components/v2-category-manager',
|
||||
'numbering-rule': '@/lib/registry/components/v2-numbering-rule',
|
||||
// ... 기타 매핑
|
||||
};
|
||||
return mapping[v1Type] || `@/lib/registry/components/v2-${v1Type}`;
|
||||
}
|
||||
```
|
||||
|
||||
### 4.4 Step 3: 카테고리 데이터 마이그레이션
|
||||
|
||||
```sql
|
||||
-- 본서버 카테고리 데이터 → 개발서버 category_values
|
||||
INSERT INTO category_values (
|
||||
table_name, column_name, value_code, value_label,
|
||||
value_order, parent_value_id, depth, company_code
|
||||
)
|
||||
SELECT
|
||||
-- V1 카테고리 데이터를 table_name + column_name 기반으로 변환
|
||||
'inspection_standard' as table_name,
|
||||
'inspection_method' as column_name,
|
||||
value_code,
|
||||
value_label,
|
||||
sort_order,
|
||||
NULL as parent_value_id,
|
||||
1 as depth,
|
||||
'COMPANY_7' as company_code
|
||||
FROM [본서버_카테고리_데이터];
|
||||
```
|
||||
|
||||
### 4.5 Step 4: 채번 규칙 마이그레이션
|
||||
|
||||
```sql
|
||||
-- 본서버 채번 규칙 → 개발서버 numbering_rules
|
||||
INSERT INTO numbering_rules (
|
||||
rule_id, rule_name, table_name, column_name,
|
||||
separator, reset_period, current_sequence, company_code
|
||||
)
|
||||
SELECT
|
||||
rule_id,
|
||||
rule_name,
|
||||
'inspection_standard' as table_name,
|
||||
'inspection_code' as column_name,
|
||||
separator,
|
||||
reset_period,
|
||||
0 as current_sequence, -- 시퀀스 초기화
|
||||
'COMPANY_7' as company_code
|
||||
FROM [본서버_채번_규칙];
|
||||
```
|
||||
|
||||
### 4.6 Step 5: table_type_columns 설정
|
||||
|
||||
```sql
|
||||
-- 카테고리 컬럼 설정
|
||||
UPDATE table_type_columns
|
||||
SET input_type = 'category'
|
||||
WHERE table_name = 'inspection_standard'
|
||||
AND column_name = 'inspection_method'
|
||||
AND company_code = 'COMPANY_7';
|
||||
|
||||
-- 채번 컬럼 설정
|
||||
UPDATE table_type_columns
|
||||
SET
|
||||
input_type = 'numbering',
|
||||
detail_settings = '{"numberingRuleId": "rule-xxx"}'
|
||||
WHERE table_name = 'inspection_standard'
|
||||
AND column_name = 'inspection_code'
|
||||
AND company_code = 'COMPANY_7';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 품질관리 메뉴 마이그레이션 현황
|
||||
|
||||
### 5.1 화면 매핑 현황
|
||||
|
||||
| 본서버 코드 | 화면명 | 테이블 | 개발서버 상태 | 비고 |
|
||||
|-------------|--------|--------|---------------|------|
|
||||
| COMPANY_7_126 | 검사정보 관리 | inspection_standard | ✅ V2 존재 | 컴포넌트 수 확인 필요 |
|
||||
| COMPANY_7_127 | 품목옵션 설정 | - | ✅ V2 존재 | v2-category-manager 사용중 |
|
||||
| COMPANY_7_138 | 카테고리 설정 | inspection_standard | ❌ 누락 | V2: table_name 기반으로 변경 |
|
||||
| COMPANY_7_139 | 코드 설정 | inspection_standard | ❌ 누락 | V2: table_name 기반으로 변경 |
|
||||
| COMPANY_7_142 | 검사장비 관리 | inspection_equipment_mng | ❌ 누락 | 모달 통합 필요 |
|
||||
| COMPANY_7_143 | 검사장비 등록모달 | inspection_equipment_mng | ❌ 누락 | COMPANY_7_142에 통합 |
|
||||
| COMPANY_7_144 | 불량기준 정보 | defect_standard_mng | ❌ 누락 | 모달 통합 필요 |
|
||||
| COMPANY_7_145 | 불량기준 등록모달 | defect_standard_mng | ❌ 누락 | COMPANY_7_144에 통합 |
|
||||
|
||||
### 5.2 카테고리/채번 컬럼 현황
|
||||
|
||||
**inspection_standard:**
|
||||
| 컬럼 | input_type | 라벨 |
|
||||
|------|------------|------|
|
||||
| inspection_method | category | 검사방법 |
|
||||
| unit | category | 단위 |
|
||||
| apply_type | category | 적용구분 |
|
||||
| inspection_type | category | 유형 |
|
||||
|
||||
**inspection_equipment_mng:**
|
||||
| 컬럼 | input_type | 라벨 |
|
||||
|------|------------|------|
|
||||
| equipment_type | category | 장비유형 |
|
||||
| installation_location | category | 설치장소 |
|
||||
| equipment_status | category | 장비상태 |
|
||||
|
||||
**defect_standard_mng:**
|
||||
| 컬럼 | input_type | 라벨 |
|
||||
|------|------------|------|
|
||||
| defect_type | category | 불량유형 |
|
||||
| severity | category | 심각도 |
|
||||
| inspection_type | category | 검사유형 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 자동화 스크립트
|
||||
|
||||
### 6.1 마이그레이션 실행 스크립트
|
||||
|
||||
```typescript
|
||||
// backend-node/src/scripts/migrateV1toV2.ts
|
||||
import { getPool } from "../database/db";
|
||||
|
||||
interface MigrationResult {
|
||||
screenCode: string;
|
||||
success: boolean;
|
||||
message: string;
|
||||
componentCount?: number;
|
||||
}
|
||||
|
||||
async function migrateScreenToV2(
|
||||
screenCode: string,
|
||||
companyCode: string
|
||||
): Promise<MigrationResult> {
|
||||
const pool = getPool();
|
||||
|
||||
try {
|
||||
// 1. V1 레이아웃 조회 (본서버에서)
|
||||
const v1Result = await pool.query(`
|
||||
SELECT sl.*, sd.table_name, sd.screen_name
|
||||
FROM screen_layouts sl
|
||||
JOIN screen_definitions sd ON sl.screen_id = sd.screen_id
|
||||
WHERE sd.screen_code = $1
|
||||
ORDER BY sl.display_order
|
||||
`, [screenCode]);
|
||||
|
||||
if (v1Result.rows.length === 0) {
|
||||
return { screenCode, success: false, message: "V1 레이아웃 없음" };
|
||||
}
|
||||
|
||||
// 2. V2 형식으로 변환
|
||||
const components = v1Result.rows
|
||||
.filter(row => row.component_type !== '_metadata')
|
||||
.map(row => ({
|
||||
id: row.component_id || `comp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
url: mapComponentUrl(row.component_type),
|
||||
position: { x: row.position_x || 0, y: row.position_y || 0 },
|
||||
size: { width: row.width || 100, height: row.height || 50 },
|
||||
displayOrder: row.display_order || 0,
|
||||
overrides: extractOverrides(row.properties, row.component_type)
|
||||
}));
|
||||
|
||||
const layoutData = {
|
||||
version: "2.0",
|
||||
components,
|
||||
migratedFrom: "V1",
|
||||
migratedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
// 3. 개발서버 V2 테이블에 저장
|
||||
const screenId = v1Result.rows[0].screen_id;
|
||||
|
||||
await pool.query(`
|
||||
INSERT INTO screen_layouts_v2 (screen_id, company_code, layout_data)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (screen_id, company_code)
|
||||
DO UPDATE SET layout_data = $3, updated_at = NOW()
|
||||
`, [screenId, companyCode, JSON.stringify(layoutData)]);
|
||||
|
||||
return {
|
||||
screenCode,
|
||||
success: true,
|
||||
message: "마이그레이션 완료",
|
||||
componentCount: components.length
|
||||
};
|
||||
} catch (error: any) {
|
||||
return { screenCode, success: false, message: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
function mapComponentUrl(v1Type: string): string {
|
||||
const mapping: Record<string, string> = {
|
||||
'table-list': '@/lib/registry/components/v2-table-list',
|
||||
'button-primary': '@/lib/registry/components/v2-button-primary',
|
||||
'text-input': '@/lib/registry/components/v2-text-input',
|
||||
'select-basic': '@/lib/registry/components/v2-select',
|
||||
'date-input': '@/lib/registry/components/v2-date-input',
|
||||
'entity-search-input': '@/lib/registry/components/v2-entity-search',
|
||||
'category-manager': '@/lib/registry/components/v2-category-manager',
|
||||
'numbering-rule': '@/lib/registry/components/v2-numbering-rule',
|
||||
'tabs-widget': '@/lib/registry/components/v2-tabs-widget',
|
||||
'textarea-basic': '@/lib/registry/components/v2-textarea',
|
||||
};
|
||||
return mapping[v1Type] || `@/lib/registry/components/v2-${v1Type}`;
|
||||
}
|
||||
|
||||
function extractOverrides(properties: any, componentType: string): Record<string, any> {
|
||||
if (!properties) return {};
|
||||
|
||||
// V2 Zod 스키마 defaults와 비교하여 다른 값만 추출
|
||||
// (실제 구현 시 각 컴포넌트의 defaultConfig와 비교)
|
||||
const overrides: Record<string, any> = {};
|
||||
|
||||
// 필수 설정만 추출
|
||||
if (properties.tableName) overrides.tableName = properties.tableName;
|
||||
if (properties.columns) overrides.columns = properties.columns;
|
||||
if (properties.label) overrides.label = properties.label;
|
||||
if (properties.onClick) overrides.onClick = properties.onClick;
|
||||
|
||||
return overrides;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 검증 체크리스트
|
||||
|
||||
### 7.1 마이그레이션 전
|
||||
|
||||
- [ ] 본서버 화면 목록 확인
|
||||
- [ ] 개발서버 기존 V2 데이터 백업
|
||||
- [ ] 컴포넌트 매핑 테이블 검토
|
||||
- [ ] 카테고리/채번 데이터 분석
|
||||
|
||||
### 7.2 마이그레이션 후
|
||||
|
||||
- [ ] screen_definitions 동기화 확인
|
||||
- [ ] screen_layouts_v2 데이터 생성 확인
|
||||
- [ ] 컴포넌트 렌더링 테스트
|
||||
- [ ] 카테고리 드롭다운 동작 확인
|
||||
- [ ] 채번 규칙 동작 확인
|
||||
- [ ] 저장/수정/삭제 기능 테스트
|
||||
|
||||
### 7.3 모달 통합 확인
|
||||
|
||||
- [ ] 기존 모달 화면 → overlay 통합 완료
|
||||
- [ ] 부모-자식 데이터 연동 확인
|
||||
- [ ] 모달 열기/닫기 동작 확인
|
||||
|
||||
---
|
||||
|
||||
## 8. 롤백 계획
|
||||
|
||||
마이그레이션 실패 시 롤백 절차:
|
||||
|
||||
```sql
|
||||
-- 1. V2 레이아웃 롤백
|
||||
DELETE FROM screen_layouts_v2
|
||||
WHERE screen_id IN (
|
||||
SELECT screen_id FROM screen_definitions
|
||||
WHERE screen_code LIKE 'COMPANY_7_%'
|
||||
);
|
||||
|
||||
-- 2. 추가된 screen_definitions 롤백
|
||||
DELETE FROM screen_definitions
|
||||
WHERE screen_code IN ('신규_추가된_코드들')
|
||||
AND company_code = 'COMPANY_7';
|
||||
|
||||
-- 3. category_values 롤백
|
||||
DELETE FROM category_values
|
||||
WHERE company_code = 'COMPANY_7'
|
||||
AND created_at > '[마이그레이션_시작_시간]';
|
||||
|
||||
-- 4. numbering_rules 롤백
|
||||
DELETE FROM numbering_rules
|
||||
WHERE company_code = 'COMPANY_7'
|
||||
AND created_at > '[마이그레이션_시작_시간]';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 참고 자료
|
||||
|
||||
### 관련 코드 파일
|
||||
|
||||
- **V2 Category Manager**: `frontend/lib/registry/components/v2-category-manager/`
|
||||
- **V2 Numbering Rule**: `frontend/lib/registry/components/v2-numbering-rule/`
|
||||
- **Category Service**: `backend-node/src/services/categoryTreeService.ts`
|
||||
- **Numbering Service**: `backend-node/src/services/numberingRuleService.ts`
|
||||
|
||||
### 관련 문서
|
||||
|
||||
- [V2 컴포넌트 분석 가이드](../V2_컴포넌트_분석_가이드.md)
|
||||
- [V2 컴포넌트 연동 가이드](../V2_컴포넌트_연동_가이드.md)
|
||||
- [화면 개발 표준 가이드](../screen-implementation-guide/SCREEN_DEVELOPMENT_STANDARD.md)
|
||||
- [컴포넌트 레이아웃 V2 아키텍처](./COMPONENT_LAYOUT_V2_ARCHITECTURE.md)
|
||||
|
||||
---
|
||||
|
||||
## 변경 이력
|
||||
|
||||
| 날짜 | 작성자 | 내용 |
|
||||
|------|--------|------|
|
||||
| 2026-02-03 | DDD1542 | 초안 작성 |
|
||||
|
|
@ -13,6 +13,7 @@ import { TableOptionsProvider } from "@/contexts/TableOptionsContext";
|
|||
import { TableSearchWidgetHeightProvider } from "@/contexts/TableSearchWidgetHeightContext";
|
||||
import { useSplitPanelContext } from "@/contexts/SplitPanelContext";
|
||||
import { ActiveTabProvider } from "@/contexts/ActiveTabContext";
|
||||
import { convertV2ToLegacy, isValidV2Layout } from "@/lib/utils/layoutV2Converter";
|
||||
|
||||
interface ScreenModalState {
|
||||
isOpen: boolean;
|
||||
|
|
@ -322,12 +323,28 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
|||
try {
|
||||
setLoading(true);
|
||||
|
||||
// 화면 정보와 레이아웃 데이터 로딩
|
||||
const [screenInfo, layoutData] = await Promise.all([
|
||||
// 화면 정보와 레이아웃 데이터 로딩 (V2 API 사용으로 기본값 병합)
|
||||
const [screenInfo, v2LayoutData] = await Promise.all([
|
||||
screenApi.getScreen(screenId),
|
||||
screenApi.getLayout(screenId),
|
||||
screenApi.getLayoutV2(screenId),
|
||||
]);
|
||||
|
||||
// V2 → Legacy 변환 (기본값 병합 포함)
|
||||
let layoutData: any = null;
|
||||
if (v2LayoutData && isValidV2Layout(v2LayoutData)) {
|
||||
layoutData = convertV2ToLegacy(v2LayoutData);
|
||||
if (layoutData) {
|
||||
// screenResolution은 V2 레이아웃에서 직접 가져오기
|
||||
layoutData.screenResolution = v2LayoutData.screenResolution || layoutData.screenResolution;
|
||||
}
|
||||
}
|
||||
|
||||
// V2 레이아웃이 없으면 기존 API로 fallback
|
||||
if (!layoutData) {
|
||||
console.log("📦 V2 레이아웃 없음, 기존 API로 fallback");
|
||||
layoutData = await screenApi.getLayout(screenId);
|
||||
}
|
||||
|
||||
// 🆕 URL 파라미터 확인 (수정 모드)
|
||||
if (typeof window !== "undefined") {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"appPort": 9771
|
||||
}
|
||||
Loading…
Reference in New Issue