# 본서버 → 개발서버 마이그레이션 가이드 ## 개요 본 문서는 **본서버(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 | 초안 작성 |