# 테이블 복제 기능 구현 계획서 ## 📋 개요 테이블 타입 관리 시스템에 기존 테이블을 복제하여 새로운 테이블을 생성하는 기능을 추가합니다. 복제 시 원본 테이블의 컬럼 구조와 설정을 가져와 사용자가 수정 후 저장할 수 있도록 합니다. --- ## 🎯 주요 요구사항 ### 1. 권한 제한 - **최고 관리자(company_code = "\*" && user_type = "SUPER_ADMIN")만 사용 가능** - 일반 회사 사용자는 테이블 복제 버튼이 보이지 않음 ### 2. 복제 프로세스 1. 테이블 목록에서 복제할 테이블 선택 (체크박스) 2. "복제" 버튼 클릭 3. 기존 "테이블 생성" 모달이 "테이블 복제" 모드로 열림 4. 원본 테이블의 설정이 자동으로 채워짐: - 테이블명: 빈 상태 (사용자가 새로 입력) - 테이블 설명: 원본 설명 복사 (수정 가능) - 컬럼 정의: 원본 컬럼들이 모두 로드됨 (수정/삭제 가능) 5. 사용자가 수정 후 저장 6. 테이블명 중복 검증 (실시간) 7. DDL 실행 및 테이블 생성 ### 3. 제약사항 - **테이블명 중복 불가** (실시간 검증) - 시스템 테이블은 복제 불가 (선택 불가 처리) - 최소 1개 이상의 컬럼 필요 --- ## 🏗️ 구현 아키텍처 ### 컴포넌트 구조 ``` 📦 테이블 타입 관리 페이지 (tableMng/page.tsx) ├── 📄 테이블 목록 (기존) │ ├── 체크박스 (단일 선택 또는 다중 선택) │ └── 복제 버튼 (최고 관리자만 표시) │ └── 🔧 CreateTableModal 확장 (CreateTableModal.tsx) ├── 모드: "create" | "duplicate" ├── Props 추가: duplicateTableName?, isDuplicateMode? ├── 복제 모드 시 자동 로드: │ ├── 원본 테이블 설명 │ └── 원본 컬럼 목록 (전체 설정 포함) └── 실시간 테이블명 중복 검증 ``` --- ## 📝 상세 구현 계획 ### Phase 1: UI 추가 (테이블 목록 페이지) #### 1.1. 체크박스 추가 **파일**: `frontend/app/(main)/admin/tableMng/page.tsx` **변경사항**: - 각 테이블 행에 체크박스 추가 - 선택된 테이블 상태 관리: `selectedTableIds` (Set) - 시스템 테이블은 체크박스 비활성화 ```typescript // 상태 추가 const [selectedTableIds, setSelectedTableIds] = useState>( new Set() ); // 최고 관리자 여부 확인 수정 (기존 코드 수정 필요) // ❌ 기존: const isSuperAdmin = user?.companyCode === "*"; // ✅ 수정: const isSuperAdmin = user?.companyCode === "*" && user?.userType === "SUPER_ADMIN"; // 시스템 테이블 목록 (복제 불가) const SYSTEM_TABLES = [ "user_info", "company_info", "menu_info", "auth_group", "menu_auth_group", "user_auth", "dept_info", "code_info", "code_category", // ... 기타 시스템 테이블 ]; // 체크박스 핸들러 const handleTableSelect = (tableName: string) => { setSelectedTableIds((prev) => { const newSet = new Set(prev); if (newSet.has(tableName)) { newSet.delete(tableName); } else { newSet.add(tableName); } return newSet; }); }; ``` #### 1.2. 복제 버튼 추가 **위치**: 테이블 목록 상단 (새 테이블 생성 버튼 옆) ```typescript { isSuperAdmin && ( ); } ``` **조건**: - 최고 관리자만 표시 - 정확히 1개의 테이블이 선택된 경우만 활성화 - 선택된 테이블이 시스템 테이블인 경우 비활성화 --- ### Phase 2: CreateTableModal 확장 #### 2.1. Props 확장 **파일**: `frontend/components/admin/CreateTableModal.tsx` ```typescript export interface CreateTableModalProps { isOpen: boolean; onClose: () => void; onSuccess: (result: any) => void; // 🆕 복제 모드 관련 mode?: "create" | "duplicate"; sourceTableName?: string; // 복제 대상 테이블명 } ``` #### 2.2. 복제 모드 감지 및 데이터 로드 ```typescript export function CreateTableModal({ isOpen, onClose, onSuccess, mode = "create", sourceTableName, }: CreateTableModalProps) { const isDuplicateMode = mode === "duplicate" && sourceTableName; // 복제 모드일 때 원본 테이블 정보 로드 useEffect(() => { if (isOpen && isDuplicateMode && sourceTableName) { loadSourceTableData(sourceTableName); } }, [isOpen, isDuplicateMode, sourceTableName]); const loadSourceTableData = async (tableName: string) => { setLoading(true); try { // 1. 테이블 기본 정보 조회 const tableResponse = await apiClient.get( `/table-management/tables/${tableName}` ); if (tableResponse.data.success) { const tableInfo = tableResponse.data.data; setDescription(tableInfo.description || ""); } // 2. 컬럼 정보 조회 const columnsResponse = await tableManagementApi.getColumnList(tableName); if (columnsResponse.success && columnsResponse.data) { const loadedColumns: CreateColumnDefinition[] = columnsResponse.data.map((col, idx) => ({ name: col.columnName, label: col.displayName || col.columnName, inputType: col.webType as any, nullable: col.isNullable === "YES", order: idx + 1, description: col.description, codeCategory: col.codeCategory || undefined, referenceTable: col.referenceTable || undefined, referenceColumn: col.referenceColumn || undefined, displayColumn: col.displayColumn || undefined, })); setColumns(loadedColumns); toast.success(`${tableName} 테이블 정보를 불러왔습니다.`); } } catch (error: any) { console.error("원본 테이블 정보 로드 실패:", error); toast.error("원본 테이블 정보를 불러오는데 실패했습니다."); onClose(); } finally { setLoading(false); } }; } ``` #### 2.3. 모달 제목 및 버튼 텍스트 변경 ```typescript return ( {isDuplicateMode ? "테이블 복제" : "새 테이블 생성"} {isDuplicateMode ? `${sourceTableName} 테이블을 복제하여 새 테이블을 생성합니다.` : "데이터베이스에 새로운 테이블을 생성합니다."} {/* ... 폼 내용 ... */} ); ``` #### 2.4. 테이블명 실시간 중복 검증 강화 ```typescript // 테이블명 변경 시 실시간 검증 const handleTableNameChange = async (name: string) => { setTableName(name); // 기본 검증 const error = validateTableName(name); if (error) { setTableNameError(error); return; } // 중복 검증 setValidating(true); try { const result = await tableManagementApi.checkTableExists(name); if (result.success && result.data?.exists) { setTableNameError("이미 존재하는 테이블명입니다."); } else { setTableNameError(""); } } catch (error) { console.error("테이블명 검증 오류:", error); } finally { setValidating(false); } }; ``` --- ### Phase 3: 백엔드 API 구현 (필요 시) #### 3.1. 테이블 기본 정보 조회 API **엔드포인트**: `GET /api/table-management/tables/:tableName` **응답**: ```json { "success": true, "data": { "tableName": "contracts", "displayName": "계약 관리", "description": "계약 정보를 관리하는 테이블", "columnCount": 15 } } ``` **구현 위치**: `backend-node/src/controllers/tableManagementController.ts` ```typescript /** * 특정 테이블의 기본 정보 조회 */ async getTableInfo(req: Request, res: Response) { const { tableName } = req.params; try { const query = ` SELECT t.table_name as "tableName", dt.display_name as "displayName", dt.description as "description", COUNT(c.column_name) as "columnCount" FROM information_schema.tables t LEFT JOIN db_table_types dt ON dt.table_name = t.table_name LEFT JOIN information_schema.columns c ON c.table_name = t.table_name WHERE t.table_schema = 'public' AND t.table_name = $1 GROUP BY t.table_name, dt.display_name, dt.description `; const result = await pool.query(query, [tableName]); if (result.rows.length === 0) { return res.status(404).json({ success: false, message: `테이블 '${tableName}'을 찾을 수 없습니다.` }); } return res.json({ success: true, data: result.rows[0] }); } catch (error: any) { logger.error("테이블 정보 조회 실패", { tableName, error: error.message }); return res.status(500).json({ success: false, message: "테이블 정보를 조회하는데 실패했습니다." }); } } ``` #### 3.2. 라우트 추가 **파일**: `backend-node/src/routes/tableManagement.ts` ```typescript // 테이블 기본 정보 조회 (새로 추가) router.get("/tables/:tableName", controller.getTableInfo); ``` --- ### Phase 4: 통합 및 테스트 #### 4.1. 테이블 목록 페이지에서 복제 흐름 구현 ```typescript // tableMng/page.tsx const handleDuplicateTable = async () => { // 선택된 테이블 1개 확인 if (selectedTableIds.size !== 1) { toast.error("복제할 테이블을 1개만 선택해주세요."); return; } const sourceTable = Array.from(selectedTableIds)[0]; // 시스템 테이블 체크 if (SYSTEM_TABLES.includes(sourceTable)) { toast.error("시스템 테이블은 복제할 수 없습니다."); return; } // 복제 모달 열기 setDuplicateSourceTable(sourceTable); setCreateTableModalMode("duplicate"); setCreateTableModalOpen(true); }; // 모달 컴포넌트 { setCreateTableModalOpen(false); setDuplicateSourceTable(null); setCreateTableModalMode("create"); }} onSuccess={(result) => { loadTables(); // 테이블 목록 새로고침 setSelectedTableIds(new Set()); // 선택 초기화 }} mode={createTableModalMode} sourceTableName={duplicateSourceTable} />; ``` --- ## 🧪 테스트 시나리오 ### 테스트 케이스 1: 정상 복제 흐름 1. 최고 관리자로 로그인 2. 테이블 타입 관리 페이지 접속 3. 복제할 테이블 1개 선택 (예: `contracts`) 4. "테이블 복제" 버튼 클릭 5. 모달이 열리고 원본 테이블 정보가 자동으로 로드됨 6. 새 테이블명 입력 (예: `contracts_backup`) 7. 컬럼 정보 확인 및 필요 시 수정 8. "복제 생성" 버튼 클릭 9. 테이블이 생성되고 목록에 표시됨 10. 성공 토스트 메시지 확인 ### 테스트 케이스 2: 테이블명 중복 검증 1. 테이블 복제 흐름 시작 2. 기존에 존재하는 테이블명 입력 (예: `user_info`) 3. 실시간으로 에러 메시지 표시: "이미 존재하는 테이블명입니다." 4. "복제 생성" 버튼 비활성화 확인 ### 테스트 케이스 3: 시스템 테이블 복제 제한 1. 시스템 테이블 선택 시도 (예: `user_info`) 2. 체크박스가 비활성화되어 있음 3. 또는 선택 시 경고 메시지: "시스템 테이블은 복제할 수 없습니다." ### 테스트 케이스 4: 권한 제한 1. 일반 회사 사용자로 로그인 2. 테이블 타입 관리 페이지 접속 3. "테이블 복제" 버튼이 보이지 않음 4. 체크박스도 표시되지 않음 (선택 사항) ### 테스트 케이스 5: 컬럼 수정 후 복제 1. 테이블 복제 모달 열기 2. 원본 컬럼 중 일부 삭제 3. 새 컬럼 추가 4. 컬럼 라벨 변경 5. 저장 시 수정된 구조로 테이블 생성됨 --- ## 📊 데이터 흐름도 ``` ┌─────────────────────────────────────────────────────────────┐ │ 1. 테이블 선택 (체크박스) │ │ - 사용자가 복제할 테이블 1개 선택 │ └────────────────────┬────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ 2. "테이블 복제" 버튼 클릭 │ │ - handleDuplicateTable() 실행 │ └────────────────────┬────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ 3. CreateTableModal 열림 (복제 모드) │ │ - mode: "duplicate" │ │ - sourceTableName: 선택된 테이블명 │ └────────────────────┬────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ 4. 원본 테이블 정보 자동 로드 │ │ - GET /api/table-management/tables/:tableName │ │ → 테이블 설명 가져오기 │ │ - GET /api/table-management/tables/:tableName/columns │ │ → 모든 컬럼 정보 가져오기 │ └────────────────────┬────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ 5. 사용자 편집 │ │ - 새 테이블명 입력 (실시간 중복 검증) │ │ - 테이블 설명 수정 │ │ - 컬럼 추가/삭제/수정 │ └────────────────────┬────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ 6. "복제 생성" 버튼 클릭 │ │ - POST /api/ddl/create-table │ │ (기존 테이블 생성 API 재사용) │ └────────────────────┬────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ 7. DDL 실행 및 테이블 생성 │ │ - CREATE TABLE ... (새 테이블명) │ │ - db_column_types 레코드 생성 │ │ - db_table_types 레코드 생성 │ └────────────────────┬────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ 8. 성공 처리 │ │ - 모달 닫기 │ │ - 테이블 목록 새로고침 │ │ - 성공 토스트 메시지 표시 │ └─────────────────────────────────────────────────────────────┘ ``` --- ## 🚀 구현 순서 (우선순위) ### Step 0: 기존 코드 수정 (최고 관리자 체크 로직) 1. ✅ `frontend/app/(main)/admin/tableMng/page.tsx` - line 101 - 기존: `const isSuperAdmin = user?.companyCode === "*";` - 수정: `const isSuperAdmin = user?.companyCode === "*" && user?.userType === "SUPER_ADMIN";` **예상 소요 시간**: 5분 ### Step 1: UI 기반 구조 (프론트엔드) 1. ✅ 테이블 목록에 체크박스 추가 2. ✅ 복제 버튼 추가 (최고 관리자만 표시) 3. ✅ 선택된 테이블 상태 관리 4. ✅ 시스템 테이블 복제 제한 **예상 소요 시간**: 1-2시간 ### Step 2: CreateTableModal 확장 1. ✅ Props 확장 (mode, sourceTableName) 2. ✅ 복제 모드 감지 로직 3. ✅ 원본 테이블 정보 로드 함수 4. ✅ 모달 제목 및 버튼 텍스트 동적 변경 5. ✅ 테이블명 실시간 중복 검증 **예상 소요 시간**: 2-3시간 ### Step 3: 백엔드 API 추가 (필요 시) 1. ✅ 테이블 기본 정보 조회 API 추가 2. ✅ 라우트 추가 3. ✅ 권한 검증 (최고 관리자만) **예상 소요 시간**: 1시간 ### Step 4: 통합 및 테스트 1. ✅ 전체 흐름 통합 2. ✅ 각 테스트 케이스 실행 3. ✅ 버그 수정 및 최적화 **예상 소요 시간**: 2-3시간 **전체 예상 소요 시간**: **6-9시간** (기존 코드 수정 포함) --- ## 🔒 보안 고려사항 ### 1. 권한 체크 - 프론트엔드: `isSuperAdmin` 플래그로 UI 제어 - `user?.companyCode === "*" && user?.userType === "SUPER_ADMIN"` - 백엔드: 모든 API 호출 시 두 가지 조건 모두 검증 ```typescript // 백엔드 미들웨어 (기존 requireSuperAdmin 재사용) import { requireSuperAdmin } from "@/middleware/superAdminMiddleware"; // 또는 인라인 체크 const requireSuperAdmin = (req: Request, res: Response, next: NextFunction) => { if (req.user?.companyCode !== "*" || req.user?.userType !== "SUPER_ADMIN") { return res.status(403).json({ success: false, message: "최고 관리자만 접근할 수 있습니다.", }); } next(); }; // 라우트 적용 router.post( "/tables/:tableName/duplicate", requireSuperAdmin, controller.duplicateTable ); ``` ### 2. 시스템 테이블 보호 - SYSTEM_TABLES 상수로 관리 - 복제 시도 시 서버 측에서도 검증 ### 3. 테이블명 검증 - SQL Injection 방지: 정규식 검증 (`^[a-z][a-z0-9_]*$`) - 예약어 체크 - 길이 제한 (3-63자) --- ## 📌 주요 체크포인트 ### 사용자 경험 (UX) - ✅ 복제 버튼은 1개 테이블 선택 시에만 활성화 - ✅ 로딩 상태 표시 (모달 열릴 때) - ✅ 실시간 테이블명 중복 검증 - ✅ 명확한 에러 메시지 - ✅ 성공/실패 토스트 알림 ### 데이터 일관성 - ✅ 원본 테이블의 모든 컬럼 정보 정확히 복사 - ✅ webType, codeCategory, referenceTable 등 관계 정보 포함 - ✅ 컬럼 순서 유지 ### 성능 - ✅ 컬럼이 많은 테이블도 빠르게 로드 - ✅ 불필요한 API 호출 최소화 - ✅ 로딩 중 중복 요청 방지 --- ## 🎨 UI 스타일 가이드 ### 복제 버튼 ```tsx ``` ### 체크박스 (테이블 행) ```tsx handleTableSelect(table.tableName)} disabled={SYSTEM_TABLES.includes(table.tableName)} className="h-4 w-4" /> ``` ### 모달 제목 (복제 모드) ```tsx 테이블 복제 {sourceTableName} 테이블을 복제하여 새 테이블을 생성합니다. ``` --- ## 📚 참고 자료 ### 기존 구현 파일 - `frontend/app/(main)/admin/tableMng/page.tsx` - 테이블 타입 관리 페이지 - `frontend/components/admin/CreateTableModal.tsx` - 테이블 생성 모달 - `frontend/components/admin/ColumnDefinitionTable.tsx` - 컬럼 정의 테이블 - `frontend/lib/api/tableManagement.ts` - 테이블 관리 API - `backend-node/src/controllers/tableManagementController.ts` - 백엔드 컨트롤러 ### 유사 기능 참고 - Yard 레이아웃 복제 기능: `YardLayoutList.tsx` (onDuplicate 핸들러) - 메뉴 복제 기능 (있다면) --- ## ✅ 완료 기준 ### Phase 1 완료 조건 - [ ] 테이블 목록에 체크박스 추가됨 - [ ] 복제 버튼 추가 (최고 관리자만 보임) - [ ] 1개 테이블 선택 시에만 버튼 활성화 - [ ] 시스템 테이블은 선택 불가 ### Phase 2 완료 조건 - [ ] CreateTableModal이 복제 모드를 지원 - [ ] 원본 테이블 정보가 자동으로 로드됨 - [ ] 모달 제목과 버튼 텍스트가 모드에 따라 변경됨 - [ ] 테이블명 실시간 중복 검증 작동 ### Phase 3 완료 조건 - [ ] 테이블 기본 정보 조회 API 추가 - [ ] 최고 관리자 권한 검증 - [ ] 라우트 등록 ### Phase 4 완료 조건 - [ ] 모든 테스트 케이스 통과 - [ ] 버그 수정 완료 - [ ] 사용자 가이드 문서 작성 --- ## 🐛 예상 이슈 및 해결 방안 ### 이슈 1: 컬럼이 너무 많은 테이블 복제 시 느림 **해결방안**: - 페이지네이션 없이 전체 컬럼 한 번에 로드 (`size=9999`) - 로딩 스피너 표시 - 필요시 API에서 캐싱 적용 ### 이슈 2: 복제 후 테이블명 입력 잊음 **해결방안**: - 테이블명 필드를 비워두고 포커스 자동 이동 - placeholder에 예시 제공 (예: `contracts_copy`) ### 이슈 3: 시스템 테이블 실수로 복제 **해결방안**: - 프론트엔드와 백엔드 양쪽에서 검증 - SYSTEM_TABLES 리스트 관리 ### 이슈 4: 참조 무결성 (Foreign Key) 복사 여부 **해결방안**: - 현재는 컬럼 구조만 복사 - Foreign Key는 사용자가 수동으로 설정 - 향후 고급 기능으로 제약조건 복사 추가 가능 --- ## 🔮 향후 개선 방향 (Optional) ### 1. 다중 테이블 복제 - 여러 테이블을 한 번에 복제 - 접두사 또는 접미사 자동 추가 (예: `_copy`) ### 2. 데이터 포함 복제 - 테이블 구조 + 데이터도 함께 복제 - `INSERT INTO ... SELECT` 방식 ### 3. 제약조건 복사 - Primary Key, Foreign Key, Unique, Check 등 - 인덱스도 함께 복사 ### 4. 복제 템플릿 - 자주 사용하는 테이블 구조를 템플릿으로 저장 - 빠른 복제 기능 ### 5. 복제 이력 관리 - 어떤 테이블이 어디서 복제되었는지 추적 - DDL 로그에 복제 정보 기록 --- ## 📞 문의 및 지원 - 기술 문의: 개발팀 - 사용 방법: 사용자 가이드 참조 - 버그 제보: 이슈 트래커 --- **작성일**: 2025-10-31 **작성자**: AI Assistant **버전**: 1.0 **상태**: ✅ 구현 완료 --- ## 🐛 버그 수정 내역 ### API 응답 구조 불일치 문제 **문제**: 모달이 뜨자마자 바로 닫히면서 "테이블 정보를 불러올 수 없습니다" 토스트 표시 **원인**: - 백엔드 API는 `{ columns: [], total, page, size }` 형태로 반환 - 프론트엔드는 `data`를 직접 배열로 취급 - 타입 정의: `ColumnListResponse extends ApiResponse` (잘못됨) **해결**: 1. **API 타입 수정** (`frontend/lib/api/tableManagement.ts`) ```typescript // 추가된 타입 export interface ColumnListData { columns: ColumnTypeInfo[]; total: number; page: number; size: number; totalPages: number; } // 수정된 타입 export interface ColumnListResponse extends ApiResponse {} ``` 2. **CreateTableModal 데이터 파싱 수정** ```typescript // Before (잘못됨) if ( columnsResponse.success && columnsResponse.data && columnsResponse.data.length > 0 ) { const firstColumn = columnsResponse.data[0]; } // After (올바름) if (columnsResponse.success && columnsResponse.data) { const columnsList = columnsResponse.data.columns; if (columnsList && columnsList.length > 0) { const firstColumn = columnsList[0]; } } ``` 3. **디버그 로그 추가** - API 응답 전체 로그 - 컬럼 리스트 추출 후 로그 - 에러 상황별 상세 로그 **결과**: - ✅ 복제 모드에서 테이블 정보 정상 로드 - ✅ 타입 안전성 향상 - ✅ 에러 핸들링 개선