+```
+
+### 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 반응형을 구현할 수 있습니다.
diff --git a/docs/DDD1542/RESPONSIVE_GRID_SYSTEM_ARCHITECTURE.md b/docs/DDD1542/RESPONSIVE_GRID_SYSTEM_ARCHITECTURE.md
index 42cd872b..411fdd1f 100644
--- a/docs/DDD1542/RESPONSIVE_GRID_SYSTEM_ARCHITECTURE.md
+++ b/docs/DDD1542/RESPONSIVE_GRID_SYSTEM_ARCHITECTURE.md
@@ -103,6 +103,162 @@
- 분할 패널 반응형 처리
```
+### 2.5 레이아웃 시스템 구조
+
+현재 시스템에는 두 가지 레벨의 레이아웃이 존재합니다:
+
+#### 2.5.1 화면 레이아웃 (screen_layouts_v2)
+
+화면 전체의 컴포넌트 배치를 담당합니다.
+
+```json
+// DB 구조
+{
+ "version": "2.0",
+ "components": [
+ { "id": "comp_1", "position": { "x": 100, "y": 50 }, ... },
+ { "id": "comp_2", "position": { "x": 500, "y": 50 }, ... },
+ { "id": "GridLayout_1", "position": { "x": 100, "y": 200 }, ... }
+ ]
+}
+```
+
+**현재**: absolute 포지션으로 컴포넌트 배치 → **반응형 불가**
+
+#### 2.5.2 컴포넌트 레이아웃 (GridLayout, FlexboxLayout 등)
+
+개별 레이아웃 컴포넌트 내부의 zone 배치를 담당합니다.
+
+| 컴포넌트 | 위치 | 내부 구조 | CSS Grid 사용 |
+|----------|------|-----------|---------------|
+| `GridLayout` | `layouts/grid/` | zones 배열 | ✅ 이미 사용 |
+| `FlexboxLayout` | `layouts/flexbox/` | zones 배열 | ❌ absolute |
+| `SplitLayout` | `layouts/split/` | left/right | ❌ flex |
+| `TabsLayout` | `layouts/` | tabs 배열 | ❌ 탭 구조 |
+| `CardLayout` | `layouts/card-layout/` | zones 배열 | ❌ flex |
+| `AccordionLayout` | `layouts/accordion/` | items 배열 | ❌ 아코디언 |
+
+#### 2.5.3 구조 다이어그램
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│ screen_layouts_v2 (화면 전체) │
+│ ┌──────────────────────────────────────────────────────────┐ │
+│ │ 현재: absolute 포지션 → 반응형 불가 │ │
+│ │ 변경: ResponsiveGridLayout (CSS Grid) → 반응형 가능 │ │
+│ └──────────────────────────────────────────────────────────┘ │
+│ │
+│ ┌──────────┐ ┌──────────┐ ┌─────────────────────────────┐ │
+│ │ v2-button │ │ v2-input │ │ GridLayout (컴포넌트) │ │
+│ │ (shadcn) │ │ (shadcn) │ │ ┌─────────┬─────────────┐ │ │
+│ └──────────┘ └──────────┘ │ │ zone1 │ zone2 │ │ │
+│ │ │ (이미 │ (이미 │ │ │
+│ │ │ CSS Grid│ CSS Grid) │ │ │
+│ │ └─────────┴─────────────┘ │ │
+│ └─────────────────────────────┘ │
+└─────────────────────────────────────────────────────────────────┘
+```
+
+### 2.6 기존 레이아웃 컴포넌트 호환성
+
+#### 2.6.1 GridLayout (기존 커스텀 그리드)
+
+```tsx
+// frontend/lib/registry/layouts/grid/GridLayout.tsx
+// 이미 CSS Grid를 사용하고 있음!
+
+const gridStyle: React.CSSProperties = {
+ display: "grid",
+ gridTemplateRows: `repeat(${gridConfig.rows}, 1fr)`,
+ gridTemplateColumns: `repeat(${gridConfig.columns}, 1fr)`,
+ gap: `${gridConfig.gap || 16}px`,
+};
+```
+
+**호환성**: ✅ **완전 호환**
+- GridLayout은 화면 내 하나의 컴포넌트로 취급됨
+- ResponsiveGridLayout이 GridLayout의 **위치만** 관리
+- GridLayout 내부는 기존 방식 그대로 동작
+
+#### 2.6.2 FlexboxLayout
+
+```tsx
+// frontend/lib/registry/layouts/flexbox/FlexboxLayout.tsx
+// zone 내부에서 컴포넌트를 absolute로 배치
+
+{zoneChildren.map((child) => (
+
+ {renderer.renderChild(child)}
+
+))}
+```
+
+**호환성**: ✅ **호환** (내부는 기존 방식 유지)
+- FlexboxLayout 컴포넌트 자체의 위치는 ResponsiveGridLayout이 관리
+- 내부 zone의 컴포넌트 배치는 기존 absolute 방식 유지
+
+#### 2.6.3 SplitPanelLayout (분할 패널)
+
+**호환성**: ⚠️ **별도 수정 필요**
+- 외부 위치: ResponsiveGridLayout이 관리 ✅
+- 내부 반응형: 별도 수정 필요 (모바일에서 상하 분할)
+
+#### 2.6.4 호환성 요약
+
+| 컴포넌트 | 외부 배치 | 내부 동작 | 추가 수정 |
+|----------|----------|----------|-----------|
+| **v2-button, v2-input 등** | ✅ 반응형 | ✅ shadcn 그대로 | ❌ 불필요 |
+| **GridLayout** | ✅ 반응형 | ✅ CSS Grid 그대로 | ❌ 불필요 |
+| **FlexboxLayout** | ✅ 반응형 | ⚠️ absolute 유지 | ❌ 불필요 |
+| **SplitPanelLayout** | ✅ 반응형 | ❌ 좌우 고정 | ⚠️ 내부 반응형 추가 |
+| **TabsLayout** | ✅ 반응형 | ✅ 탭 그대로 | ❌ 불필요 |
+
+### 2.7 동작 방식 비교
+
+#### 변경 전
+
+```
+화면 로드
+ ↓
+screen_layouts_v2에서 components 조회
+ ↓
+각 컴포넌트를 position.x, position.y로 absolute 배치
+ ↓
+GridLayout 컴포넌트도 absolute로 배치됨
+ ↓
+GridLayout 내부는 CSS Grid로 zone 배치
+ ↓
+결과: 화면 크기 변해도 모든 컴포넌트 위치 고정
+```
+
+#### 변경 후
+
+```
+화면 로드
+ ↓
+screen_layouts_v2에서 components 조회
+ ↓
+layoutMode === "grid" 확인
+ ↓
+ResponsiveGridLayout으로 렌더링 (CSS Grid)
+ ↓
+각 컴포넌트를 grid.col, grid.colSpan으로 배치
+ ↓
+화면 크기 감지 (ResizeObserver)
+ ↓
+breakpoint에 따라 responsive.sm/md/lg 적용
+ ↓
+GridLayout 컴포넌트도 반응형으로 배치됨
+ ↓
+GridLayout 내부는 기존 CSS Grid로 zone 배치 (변경 없음)
+ ↓
+결과: 화면 크기에 따라 컴포넌트 재배치
+```
+
---
## 3. 기술 결정
@@ -649,6 +805,10 @@ ALTER TABLE screen_layouts_v2_backup_20260130 RENAME TO screen_layouts_v2;
- [ ] 태블릿 (768px, 1024px) 테스트
- [ ] 모바일 (375px, 414px) 테스트
- [ ] 분할 패널 화면 테스트
+- [ ] GridLayout 컴포넌트 포함 화면 테스트
+- [ ] FlexboxLayout 컴포넌트 포함 화면 테스트
+- [ ] TabsLayout 컴포넌트 포함 화면 테스트
+- [ ] 중첩 레이아웃 (GridLayout 안에 컴포넌트) 테스트
---
@@ -659,6 +819,8 @@ ALTER TABLE screen_layouts_v2_backup_20260130 RENAME TO screen_layouts_v2;
| 마이그레이션 실패 | 높음 | 백업 테이블에서 즉시 롤백 |
| 기존 화면 깨짐 | 중간 | `layoutMode` 없으면 기존 방식 사용 (폴백) |
| 디자인 모드 혼란 | 낮음 | position/size 필드 유지 |
+| GridLayout 내부 깨짐 | 낮음 | 내부는 기존 방식 유지, 외부 배치만 변경 |
+| 중첩 레이아웃 문제 | 낮음 | 각 레이아웃 컴포넌트는 독립적으로 동작 |
---
diff --git a/docs/DDD1542/본서버_개발서버_마이그레이션_가이드.md b/docs/DDD1542/본서버_개발서버_마이그레이션_가이드.md
new file mode 100644
index 00000000..e8f7b39e
--- /dev/null
+++ b/docs/DDD1542/본서버_개발서버_마이그레이션_가이드.md
@@ -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 | 초안 작성 |
diff --git a/docs/DDD1542/본서버_개발서버_마이그레이션_상세가이드.md b/docs/DDD1542/본서버_개발서버_마이그레이션_상세가이드.md
new file mode 100644
index 00000000..42ce37f1
--- /dev/null
+++ b/docs/DDD1542/본서버_개발서버_마이그레이션_상세가이드.md
@@ -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
{
+ 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 = {
+ '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 {
+ if (!properties) return {};
+
+ // V2 Zod 스키마 defaults와 비교하여 다른 값만 추출
+ // (실제 구현 시 각 컴포넌트의 defaultConfig와 비교)
+ const overrides: Record = {};
+
+ // 필수 설정만 추출
+ 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 | 초안 작성 |
diff --git a/docs/DDD1542/화면관계_시각화_개선_보고서.md b/docs/DDD1542/화면관계_시각화_개선_보고서.md
index 27946afa..aea92243 100644
--- a/docs/DDD1542/화면관계_시각화_개선_보고서.md
+++ b/docs/DDD1542/화면관계_시각화_개선_보고서.md
@@ -23,7 +23,8 @@
| 테이블명 | 용도 | 주요 컬럼 |
|----------|------|----------|
| `screen_definitions` | 화면 정의 정보 | `screen_id`, `screen_name`, `table_name`, `company_code` |
-| `screen_layouts` | 화면 레이아웃/컴포넌트 정보 | `screen_id`, `properties` (JSONB - componentConfig 포함) |
+| `screen_layouts` | 화면 레이아웃/컴포넌트 정보 (Legacy) | `screen_id`, `properties` (JSONB - componentConfig 포함) |
+| `screen_layouts_v2` | 화면 레이아웃/컴포넌트 정보 (V2) | `screen_id`, `layout_data` (JSONB - components 배열) |
| `screen_groups` | 화면 그룹 정보 | `group_id`, `group_code`, `group_name`, `parent_group_id` |
| `screen_group_mappings` | 화면-그룹 매핑 | `group_id`, `screen_id`, `display_order` |
@@ -86,9 +87,17 @@ screen_groups (그룹)
│ │
│ └─── screen_definitions (화면)
│ │
- │ └─── screen_layouts (레이아웃/컴포넌트)
+ │ ├─── screen_layouts (Legacy)
+ │ │ │
+ │ │ └─── properties.componentConfig
+ │ │ ├── fieldMappings
+ │ │ ├── parentDataMapping
+ │ │ ├── columns.mapping
+ │ │ └── rightPanel.relation
+ │ │
+ │ └─── screen_layouts_v2 (V2) ← 현재 표준
│ │
- │ └─── properties.componentConfig
+ │ └─── layout_data.components[].overrides
│ ├── fieldMappings
│ ├── parentDataMapping
│ ├── columns.mapping
@@ -1120,9 +1129,12 @@ screenSubTables[screenId].subTables.push({
21. [x] 필터 연결선 포커싱 제어 (해당 화면 포커싱 시에만 표시)
22. [x] 저장 테이블 제외 조건 추가 (table-list + 체크박스 + openModalWithData)
23. [x] 첫 진입 시 포커싱 없이 시작 (트리에서 화면 클릭 시 그룹만 진입)
-24. [ ] **선 교차점 이질감 해결** (계획 중)
-22. [ ] 범례 UI 추가 (선택사항)
-23. [ ] 엣지 라벨에 관계 유형 표시 (선택사항)
+24. [x] **screen_layouts_v2 지원 추가** (rightPanel.relation V2 UNION 쿼리) ✅ 2026-01-30
+25. [x] **테이블 분류 우선순위 시스템** (메인 > 서브 우선순위 적용) ✅ 2026-01-30
+26. [x] **globalMainTables API 추가** (WHERE 조건 대상 테이블 목록 반환) ✅ 2026-01-30
+27. [ ] **선 교차점 이질감 해결** (계획 중)
+28. [ ] 범례 UI 추가 (선택사항)
+29. [ ] 엣지 라벨에 관계 유형 표시 (선택사항)
---
@@ -1682,6 +1694,149 @@ frontend/
---
+## 테이블 분류 우선순위 시스템 (2026-01-30)
+
+### 배경
+
+마스터-디테일 관계의 디테일 테이블(예: `user_dept`)이 다른 곳에서 autocomplete 참조로도 사용되는 경우,
+서브 테이블 영역에 잘못 배치되는 문제가 발생했습니다.
+
+### 문제 상황
+
+```
+[user_info] - 화면 139의 디테일 → 메인 테이블 영역 (O)
+[user_dept] - 화면 162의 디테일이지만 autocomplete 참조도 있음 → 서브 테이블 영역 (X)
+```
+
+**원인**: 테이블 분류 시 우선순위가 없어서 먼저 발견된 관계 타입으로 분류됨
+
+### 해결책: 우선순위 기반 테이블 분류
+
+#### 분류 규칙
+
+| 우선순위 | 분류 | 조건 | 비고 |
+|----------|------|------|------|
+| **1순위** | 메인 테이블 | `screen_definitions.table_name` | 컴포넌트 직접 연결 |
+| **1순위** | 메인 테이블 | `v2-split-panel-layout.rightPanel.tableName` | WHERE 조건 대상 |
+| **2순위** | 서브 테이블 | 조인으로만 연결된 테이블 | autocomplete 등 참조 |
+
+#### 핵심 규칙
+
+> **메인 조건에 해당하면, 서브 조건이 있어도 무조건 메인으로 분류**
+
+### 백엔드 변경 (`screenGroupController.ts`)
+
+#### 1. screen_layouts_v2 지원 추가
+
+`rightPanelQuery`에 V2 테이블 UNION 추가:
+
+```sql
+-- V1: screen_layouts에서 조회
+SELECT ...
+FROM screen_definitions sd
+JOIN screen_layouts sl ON sd.screen_id = sl.screen_id
+WHERE sl.properties->'componentConfig'->'rightPanel'->'relation' IS NOT NULL
+
+UNION ALL
+
+-- V2: screen_layouts_v2에서 조회 (v2-split-panel-layout 컴포넌트)
+SELECT
+ sd.screen_id,
+ comp->'overrides'->>'type' as component_type,
+ comp->'overrides'->'rightPanel'->'relation' as right_panel_relation,
+ comp->'overrides'->'rightPanel'->>'tableName' as right_panel_table,
+ ...
+FROM screen_definitions sd
+JOIN screen_layouts_v2 slv2 ON sd.screen_id = slv2.screen_id,
+jsonb_array_elements(slv2.layout_data->'components') as comp
+WHERE comp->'overrides'->'rightPanel'->'relation' IS NOT NULL
+```
+
+#### 2. globalMainTables API 추가
+
+`getScreenSubTables` 응답에 전역 메인 테이블 목록 추가:
+
+```sql
+-- 모든 화면의 메인 테이블 수집
+SELECT DISTINCT table_name as main_table FROM screen_definitions WHERE screen_id = ANY($1)
+UNION
+SELECT DISTINCT comp->'overrides'->'rightPanel'->>'tableName' as main_table
+FROM screen_layouts_v2 ...
+```
+
+**응답 구조:**
+```typescript
+res.json({
+ success: true,
+ data: screenSubTables,
+ globalMainTables: globalMainTables, // 메인 테이블 목록 추가
+});
+```
+
+### 프론트엔드 변경 (`ScreenRelationFlow.tsx`)
+
+#### 1. globalMainTables 상태 추가
+
+```typescript
+const [globalMainTables, setGlobalMainTables] = useState>(new Set());
+```
+
+#### 2. 우선순위 기반 테이블 분류
+
+```typescript
+// 1. globalMainTables를 mainTableSet에 먼저 추가 (우선순위 적용)
+globalMainTables.forEach((tableName) => {
+ if (!mainTableSet.has(tableName)) {
+ mainTableSet.add(tableName);
+ filterTableSet.add(tableName); // 보라색 테두리
+ }
+});
+
+// 2. 서브 테이블 수집 (mainTableSet에 없는 것만)
+screenSubData.subTables.forEach((subTable) => {
+ if (mainTableSet.has(subTable.tableName)) {
+ return; // 메인 테이블은 서브에서 제외
+ }
+ subTableSet.add(subTable.tableName);
+});
+```
+
+### 시각적 결과
+
+#### 변경 전
+
+```
+[화면 노드들]
+ │
+ ▼
+[메인 테이블: dept_info, user_info] ← user_dept 없음
+ │
+ ▼
+[서브 테이블: user_dept, customer_mng] ← user_dept가 잘못 배치됨
+```
+
+#### 변경 후
+
+```
+[화면 노드들]
+ │
+ ▼
+[메인 테이블: dept_info, user_info, user_dept] ← user_dept 보라색 테두리
+ │
+ ▼
+[서브 테이블: customer_mng] ← 조인 참조용 테이블만
+```
+
+### 관련 파일
+
+| 파일 | 변경 내용 |
+|------|----------|
+| `backend-node/src/controllers/screenGroupController.ts` | screen_layouts_v2 UNION 추가, globalMainTables 반환 |
+| `frontend/components/screen/ScreenRelationFlow.tsx` | globalMainTables 상태, 우선순위 분류 로직 |
+| `frontend/components/screen/ScreenNode.tsx` | isFilterTable prop 및 보라색 테두리 스타일 |
+
+---
+
## 화면 설정 모달 개선 (2026-01-12)
### 개요
@@ -1742,4 +1897,6 @@ npm install react-zoom-pan-pinch
- [멀티테넌시 구현 가이드](.cursor/rules/multi-tenancy-guide.mdc)
- [API 클라이언트 사용 규칙](.cursor/rules/api-client-usage.mdc)
- [관리자 페이지 스타일 가이드](.cursor/rules/admin-page-style-guide.mdc)
+- [화면 복제 V2 마이그레이션 계획서](../SCREEN_COPY_V2_MIGRATION_PLAN.md) - screen_layouts_v2 복제 로직
+- [V2 컴포넌트 마이그레이션 분석](../V2_COMPONENT_MIGRATION_ANALYSIS.md) - V2 아키텍처
diff --git a/docs/SCREEN_COPY_V2_MIGRATION_PLAN.md b/docs/SCREEN_COPY_V2_MIGRATION_PLAN.md
index 7e1afcba..c60f1dfb 100644
--- a/docs/SCREEN_COPY_V2_MIGRATION_PLAN.md
+++ b/docs/SCREEN_COPY_V2_MIGRATION_PLAN.md
@@ -467,9 +467,9 @@ V2 전환 롤백 (필요시):
- [x] copyScreens() - Legacy 제거, V2로 교체 ✅ 2026-01-28
- [x] hasLayoutChangesV2() 함수 추가 ✅ 2026-01-28
- [x] updateTabScreenReferences() V2 지원 추가 ✅ 2026-01-28
-- [ ] 단위 테스트 통과
-- [ ] 통합 테스트 통과
-- [ ] V2 전용 복제 동작 확인
+- [x] 단위 테스트 통과 ✅ 2026-01-30
+- [x] 통합 테스트 통과 ✅ 2026-01-30
+- [x] V2 전용 복제 동작 확인 ✅ 2026-01-30
### 9.3 Phase 2 완료 조건
@@ -522,3 +522,4 @@ V2 전환 롤백 (필요시):
| 2026-01-28 | 시뮬레이션 검증 - updateTabScreenReferences V2 지원 추가 | Claude |
| 2026-01-28 | V2 경로 지원 추가 - action/sections 직접 경로 (componentConfig 없이) | Claude |
| 2026-01-30 | **실제 코드 구현 완료** - copyScreen(), copyScreens() V2 전환 | Claude |
+| 2026-01-30 | **Phase 1 테스트 완료** - 단위/통합 테스트 통과 확인 | Claude |
\ No newline at end of file
diff --git a/frontend/components/common/ScreenModal.tsx b/frontend/components/common/ScreenModal.tsx
index d991e553..dbb1e923 100644
--- a/frontend/components/common/ScreenModal.tsx
+++ b/frontend/components/common/ScreenModal.tsx
@@ -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 = ({ 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);
diff --git a/frontend/components/screen/ScreenNode.tsx b/frontend/components/screen/ScreenNode.tsx
index e49bf6d8..bcaaf054 100644
--- a/frontend/components/screen/ScreenNode.tsx
+++ b/frontend/components/screen/ScreenNode.tsx
@@ -58,6 +58,7 @@ export interface TableNodeData {
label: string;
subLabel?: string;
isMain?: boolean;
+ isFilterTable?: boolean; // 마스터-디테일의 디테일 테이블인지 (보라색 테두리)
isFocused?: boolean; // 포커스된 테이블인지
isFaded?: boolean; // 흑백 처리할지
columns?: Array<{
@@ -448,7 +449,7 @@ const ScreenPreview: React.FC<{ layoutSummary: ScreenLayoutSummary; screenType:
// ========== 테이블 노드 (하단) - 컬럼 목록 표시 (컴팩트) ==========
export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
- const { label, subLabel, isMain, isFocused, isFaded, columns, highlightedColumns, joinColumns, joinColumnRefs, filterColumns, fieldMappings, referencedBy, saveInfos } = data;
+ const { label, subLabel, isMain, isFilterTable, isFocused, isFaded, columns, highlightedColumns, joinColumns, joinColumnRefs, filterColumns, fieldMappings, referencedBy, saveInfos } = data;
// 강조할 컬럼 세트 (영문 컬럼명 기준)
const highlightSet = new Set(highlightedColumns || []);
@@ -574,16 +575,19 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
return (
{
+ const handleScreenListRefresh = () => {
+ // refreshKey 증가로 데이터 재로드 트리거
+ setRefreshKey(prev => prev + 1);
+ };
+
+ window.addEventListener("screen-list-refresh", handleScreenListRefresh);
+ return () => {
+ window.removeEventListener("screen-list-refresh", handleScreenListRefresh);
+ };
+ }, []);
+
// 그룹 또는 화면이 변경될 때 포커스 초기화
useEffect(() => {
setFocusedScreenId(null);
@@ -170,6 +183,10 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
// 화면별 사용 컬럼 매핑 (화면 ID -> 테이블명 -> 사용 컬럼들)
const [screenUsedColumnsMap, setScreenUsedColumnsMap] = useState>>({});
+
+ // 전역 메인 테이블 목록 (우선순위: 메인 > 서브)
+ // 이 목록에 있는 테이블은 서브 테이블로 분류되지 않음
+ const [globalMainTables, setGlobalMainTables] = useState>(new Set());
// 테이블 컬럼 정보 로드
const loadTableColumns = useCallback(
@@ -266,24 +283,26 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
const flows = flowsRes.success ? flowsRes.data || [] : [];
const relations = relationsRes.success ? relationsRes.data || [] : [];
- // 데이터 흐름에서 연결된 화면들 추가
- flows.forEach((flow: any) => {
- if (flow.source_screen_id === screen.screenId && flow.target_screen_id) {
- const exists = screenList.some((s) => s.screenId === flow.target_screen_id);
- if (!exists) {
- screenList.push({
- screenId: flow.target_screen_id,
- screenName: flow.target_screen_name || `화면 ${flow.target_screen_id}`,
- screenCode: "",
- tableName: "",
- companyCode: screen.companyCode,
- isActive: "Y",
- createdDate: new Date(),
- updatedDate: new Date(),
- } as ScreenDefinition);
+ // 데이터 흐름에서 연결된 화면들 추가 (개별 화면 모드에서만 - 그룹 모드에서는 그룹 내 화면만 표시)
+ if (!selectedGroup && screen) {
+ flows.forEach((flow: any) => {
+ if (flow.source_screen_id === screen.screenId && flow.target_screen_id) {
+ const exists = screenList.some((s) => s.screenId === flow.target_screen_id);
+ if (!exists) {
+ screenList.push({
+ screenId: flow.target_screen_id,
+ screenName: flow.target_screen_name || `화면 ${flow.target_screen_id}`,
+ screenCode: "",
+ tableName: "",
+ companyCode: screen.companyCode,
+ isActive: "Y",
+ createdDate: new Date(),
+ updatedDate: new Date(),
+ } as ScreenDefinition);
+ }
}
- }
- });
+ });
+ }
// 화면 레이아웃 요약 정보 로드
const screenIds = screenList.map((s) => s.screenId);
@@ -305,6 +324,13 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
subTablesData = subTablesRes.data as Record;
// 서브 테이블 데이터 저장 (조인 컬럼 정보 포함)
setSubTablesDataMap(subTablesData);
+
+ // 전역 메인 테이블 목록 저장 (우선순위 적용용)
+ // 이 목록에 있는 테이블은 서브 테이블로 분류되지 않음
+ const globalMainTablesArr = (subTablesRes as any).globalMainTables as string[] | undefined;
+ if (globalMainTablesArr && Array.isArray(globalMainTablesArr)) {
+ setGlobalMainTables(new Set(globalMainTablesArr));
+ }
}
} catch (e) {
console.error("레이아웃 요약/서브 테이블 로드 실패:", e);
@@ -434,9 +460,27 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
if (rel.table_name) mainTableSet.add(rel.table_name);
});
- // 서브 테이블 수집 (componentConfig에서 추출된 테이블들)
- // 서브 테이블은 메인 테이블과 다른 테이블들
- // 화면별 서브 테이블 매핑도 함께 구축
+ // ============================================================
+ // 테이블 분류 (우선순위: 메인 > 서브)
+ // ============================================================
+ // 메인 테이블 조건:
+ // 1. screen_definitions.table_name (컴포넌트 직접 연결) - 이미 mainTableSet에 추가됨
+ // 2. globalMainTables (WHERE 조건 대상, 마스터-디테일의 디테일 테이블)
+ //
+ // 서브 테이블 조건:
+ // - 조인(JOIN)으로만 연결된 테이블 (autocomplete 등에서 참조)
+ // - 단, mainTableSet에 있으면 제외 (우선순위: 메인 > 서브)
+
+ // 1. globalMainTables를 mainTableSet에 먼저 추가 (우선순위 적용)
+ const filterTableSet = new Set(); // 마스터-디테일의 디테일 테이블들
+ globalMainTables.forEach((tableName) => {
+ if (!mainTableSet.has(tableName)) {
+ mainTableSet.add(tableName);
+ filterTableSet.add(tableName); // 필터 테이블로 분류 (보라색 테두리)
+ }
+ });
+
+ // 2. 서브 테이블 수집 (mainTableSet에 없는 것만)
const newScreenSubTableMap: Record = {};
Object.entries(subTablesData).forEach(([screenIdStr, screenSubData]) => {
@@ -444,11 +488,14 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
const subTableNames: string[] = [];
screenSubData.subTables.forEach((subTable) => {
- // 메인 테이블에 없는 것만 서브 테이블로 추가
- if (!mainTableSet.has(subTable.tableName)) {
- subTableSet.add(subTable.tableName);
- subTableNames.push(subTable.tableName);
+ // mainTableSet에 있으면 서브 테이블에서 제외 (우선순위: 메인 > 서브)
+ if (mainTableSet.has(subTable.tableName)) {
+ return;
}
+
+ // 조인으로만 연결된 테이블 → 서브 테이블
+ subTableSet.add(subTable.tableName);
+ subTableNames.push(subTable.tableName);
});
if (subTableNames.length > 0) {
@@ -539,10 +586,19 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
isForeignKey: !!col.referenceTable || (col.columnName?.includes("_id") && col.columnName !== "id"),
}));
- // 여러 화면이 같은 테이블 사용하면 "공통 메인 테이블", 아니면 "메인 테이블"
- const subLabel = linkedScreens.length > 1
- ? `메인 테이블 (${linkedScreens.length}개 화면)`
- : "메인 테이블";
+ // 테이블 분류에 따른 라벨 결정
+ // 1. 필터 테이블 (마스터-디테일의 디테일): "필터 대상 테이블"
+ // 2. 여러 화면이 같은 테이블 사용: "공통 메인 테이블 (N개 화면)"
+ // 3. 일반 메인 테이블: "메인 테이블"
+ const isFilterTable = filterTableSet.has(tableName);
+ let subLabel: string;
+ if (isFilterTable) {
+ subLabel = "필터 대상 테이블 (마스터-디테일)";
+ } else if (linkedScreens.length > 1) {
+ subLabel = `메인 테이블 (${linkedScreens.length}개 화면)`;
+ } else {
+ subLabel = "메인 테이블";
+ }
// 이 테이블을 참조하는 관계들
tableNodes.push({
@@ -552,7 +608,8 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
data: {
label: tableName,
subLabel: subLabel,
- isMain: true, // mainTableSet의 모든 테이블은 메인
+ isMain: !isFilterTable, // 필터 테이블은 isMain: false로 설정 (보라색 테두리 표시용)
+ isFilterTable: isFilterTable, // 필터 테이블 여부 표시
columns: formattedColumns,
// referencedBy, filterColumns, saveInfos는 styledNodes에서 포커스 상태에 따라 동적으로 설정
},