26 KiB
26 KiB
테이블 복제 기능 구현 계획서
📋 개요
테이블 타입 관리 시스템에 기존 테이블을 복제하여 새로운 테이블을 생성하는 기능을 추가합니다. 복제 시 원본 테이블의 컬럼 구조와 설정을 가져와 사용자가 수정 후 저장할 수 있도록 합니다.
🎯 주요 요구사항
1. 권한 제한
- 최고 관리자(company_code = "*" && user_type = "SUPER_ADMIN")만 사용 가능
- 일반 회사 사용자는 테이블 복제 버튼이 보이지 않음
2. 복제 프로세스
- 테이블 목록에서 복제할 테이블 선택 (체크박스)
- "복제" 버튼 클릭
- 기존 "테이블 생성" 모달이 "테이블 복제" 모드로 열림
- 원본 테이블의 설정이 자동으로 채워짐:
- 테이블명: 빈 상태 (사용자가 새로 입력)
- 테이블 설명: 원본 설명 복사 (수정 가능)
- 컬럼 정의: 원본 컬럼들이 모두 로드됨 (수정/삭제 가능)
- 사용자가 수정 후 저장
- 테이블명 중복 검증 (실시간)
- 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) - 시스템 테이블은 체크박스 비활성화
// 상태 추가
const [selectedTableIds, setSelectedTableIds] = useState<Set<string>>(
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. 복제 버튼 추가
위치: 테이블 목록 상단 (새 테이블 생성 버튼 옆)
{
isSuperAdmin && (
<Button
variant="outline"
onClick={handleDuplicateTable}
disabled={selectedTableIds.size !== 1}
className="h-9 text-xs sm:h-10 sm:text-sm"
>
<Copy className="mr-2 h-4 w-4" />
테이블 복제
</Button>
);
}
조건:
- 최고 관리자만 표시
- 정확히 1개의 테이블이 선택된 경우만 활성화
- 선택된 테이블이 시스템 테이블인 경우 비활성화
Phase 2: CreateTableModal 확장
2.1. Props 확장
파일: frontend/components/admin/CreateTableModal.tsx
export interface CreateTableModalProps {
isOpen: boolean;
onClose: () => void;
onSuccess: (result: any) => void;
// 🆕 복제 모드 관련
mode?: "create" | "duplicate";
sourceTableName?: string; // 복제 대상 테이블명
}
2.2. 복제 모드 감지 및 데이터 로드
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. 모달 제목 및 버튼 텍스트 변경
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-[95vw] sm:max-w-[800px]">
<DialogHeader>
<DialogTitle className="text-base sm:text-lg">
{isDuplicateMode ? "테이블 복제" : "새 테이블 생성"}
</DialogTitle>
<DialogDescription className="text-xs sm:text-sm">
{isDuplicateMode
? `${sourceTableName} 테이블을 복제하여 새 테이블을 생성합니다.`
: "데이터베이스에 새로운 테이블을 생성합니다."}
</DialogDescription>
</DialogHeader>
{/* ... 폼 내용 ... */}
<DialogFooter className="gap-2 sm:gap-0">
<Button
variant="outline"
onClick={onClose}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>
취소
</Button>
<Button
onClick={handleCreateTable}
disabled={!isFormValid || loading}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>
{loading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
생성 중...
</>
) : isDuplicateMode ? (
"복제 생성"
) : (
"생성"
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
2.4. 테이블명 실시간 중복 검증 강화
// 테이블명 변경 시 실시간 검증
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
응답:
{
"success": true,
"data": {
"tableName": "contracts",
"displayName": "계약 관리",
"description": "계약 정보를 관리하는 테이블",
"columnCount": 15
}
}
구현 위치: backend-node/src/controllers/tableManagementController.ts
/**
* 특정 테이블의 기본 정보 조회
*/
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
// 테이블 기본 정보 조회 (새로 추가)
router.get("/tables/:tableName", controller.getTableInfo);
Phase 4: 통합 및 테스트
4.1. 테이블 목록 페이지에서 복제 흐름 구현
// 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);
};
// 모달 컴포넌트
<CreateTableModal
isOpen={createTableModalOpen}
onClose={() => {
setCreateTableModalOpen(false);
setDuplicateSourceTable(null);
setCreateTableModalMode("create");
}}
onSuccess={(result) => {
loadTables(); // 테이블 목록 새로고침
setSelectedTableIds(new Set()); // 선택 초기화
}}
mode={createTableModalMode}
sourceTableName={duplicateSourceTable}
/>;
🧪 테스트 시나리오
테스트 케이스 1: 정상 복제 흐름
- 최고 관리자로 로그인
- 테이블 타입 관리 페이지 접속
- 복제할 테이블 1개 선택 (예:
contracts) - "테이블 복제" 버튼 클릭
- 모달이 열리고 원본 테이블 정보가 자동으로 로드됨
- 새 테이블명 입력 (예:
contracts_backup) - 컬럼 정보 확인 및 필요 시 수정
- "복제 생성" 버튼 클릭
- 테이블이 생성되고 목록에 표시됨
- 성공 토스트 메시지 확인
테스트 케이스 2: 테이블명 중복 검증
- 테이블 복제 흐름 시작
- 기존에 존재하는 테이블명 입력 (예:
user_info) - 실시간으로 에러 메시지 표시: "이미 존재하는 테이블명입니다."
- "복제 생성" 버튼 비활성화 확인
테스트 케이스 3: 시스템 테이블 복제 제한
- 시스템 테이블 선택 시도 (예:
user_info) - 체크박스가 비활성화되어 있음
- 또는 선택 시 경고 메시지: "시스템 테이블은 복제할 수 없습니다."
테스트 케이스 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: 기존 코드 수정 (최고 관리자 체크 로직)
- ✅
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시간
Step 2: CreateTableModal 확장
- ✅ Props 확장 (mode, sourceTableName)
- ✅ 복제 모드 감지 로직
- ✅ 원본 테이블 정보 로드 함수
- ✅ 모달 제목 및 버튼 텍스트 동적 변경
- ✅ 테이블명 실시간 중복 검증
예상 소요 시간: 2-3시간
Step 3: 백엔드 API 추가 (필요 시)
- ✅ 테이블 기본 정보 조회 API 추가
- ✅ 라우트 추가
- ✅ 권한 검증 (최고 관리자만)
예상 소요 시간: 1시간
Step 4: 통합 및 테스트
- ✅ 전체 흐름 통합
- ✅ 각 테스트 케이스 실행
- ✅ 버그 수정 및 최적화
예상 소요 시간: 2-3시간
전체 예상 소요 시간: 6-9시간 (기존 코드 수정 포함)
🔒 보안 고려사항
1. 권한 체크
- 프론트엔드:
isSuperAdmin플래그로 UI 제어user?.companyCode === "*" && user?.userType === "SUPER_ADMIN"
- 백엔드: 모든 API 호출 시 두 가지 조건 모두 검증
// 백엔드 미들웨어 (기존 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 스타일 가이드
복제 버튼
<Button
variant="outline"
onClick={handleDuplicateTable}
disabled={selectedTableIds.size !== 1}
className="h-9 text-xs sm:h-10 sm:text-sm"
>
<Copy className="mr-2 h-4 w-4" />
테이블 복제
</Button>
체크박스 (테이블 행)
<Checkbox
checked={selectedTableIds.has(table.tableName)}
onCheckedChange={() => handleTableSelect(table.tableName)}
disabled={SYSTEM_TABLES.includes(table.tableName)}
className="h-4 w-4"
/>
모달 제목 (복제 모드)
<DialogTitle className="text-base sm:text-lg">
테이블 복제
</DialogTitle>
<DialogDescription className="text-xs sm:text-sm">
{sourceTableName} 테이블을 복제하여 새 테이블을 생성합니다.
</DialogDescription>
📚 참고 자료
기존 구현 파일
frontend/app/(main)/admin/tableMng/page.tsx- 테이블 타입 관리 페이지frontend/components/admin/CreateTableModal.tsx- 테이블 생성 모달frontend/components/admin/ColumnDefinitionTable.tsx- 컬럼 정의 테이블frontend/lib/api/tableManagement.ts- 테이블 관리 APIbackend-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<ColumnTypeInfo[]>(잘못됨)
해결:
-
API 타입 수정 (
frontend/lib/api/tableManagement.ts)// 추가된 타입 export interface ColumnListData { columns: ColumnTypeInfo[]; total: number; page: number; size: number; totalPages: number; } // 수정된 타입 export interface ColumnListResponse extends ApiResponse<ColumnListData> {} -
CreateTableModal 데이터 파싱 수정
// 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]; } } -
디버그 로그 추가
- API 응답 전체 로그
- 컬럼 리스트 추출 후 로그
- 에러 상황별 상세 로그
결과:
- ✅ 복제 모드에서 테이블 정보 정상 로드
- ✅ 타입 안전성 향상
- ✅ 에러 핸들링 개선