880 lines
26 KiB
Markdown
880 lines
26 KiB
Markdown
# 테이블 복제 기능 구현 계획서
|
|
|
|
## 📋 개요
|
|
|
|
테이블 타입 관리 시스템에 기존 테이블을 복제하여 새로운 테이블을 생성하는 기능을 추가합니다. 복제 시 원본 테이블의 컬럼 구조와 설정을 가져와 사용자가 수정 후 저장할 수 있도록 합니다.
|
|
|
|
---
|
|
|
|
## 🎯 주요 요구사항
|
|
|
|
### 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<string>)
|
|
- 시스템 테이블은 체크박스 비활성화
|
|
|
|
```typescript
|
|
// 상태 추가
|
|
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. 복제 버튼 추가
|
|
|
|
**위치**: 테이블 목록 상단 (새 테이블 생성 버튼 옆)
|
|
|
|
```typescript
|
|
{
|
|
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`
|
|
|
|
```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 (
|
|
<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. 테이블명 실시간 중복 검증 강화
|
|
|
|
```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);
|
|
};
|
|
|
|
// 모달 컴포넌트
|
|
<CreateTableModal
|
|
isOpen={createTableModalOpen}
|
|
onClose={() => {
|
|
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
|
|
<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>
|
|
```
|
|
|
|
### 체크박스 (테이블 행)
|
|
|
|
```tsx
|
|
<Checkbox
|
|
checked={selectedTableIds.has(table.tableName)}
|
|
onCheckedChange={() => handleTableSelect(table.tableName)}
|
|
disabled={SYSTEM_TABLES.includes(table.tableName)}
|
|
className="h-4 w-4"
|
|
/>
|
|
```
|
|
|
|
### 모달 제목 (복제 모드)
|
|
|
|
```tsx
|
|
<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` - 테이블 관리 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<ColumnTypeInfo[]>` (잘못됨)
|
|
|
|
**해결**:
|
|
|
|
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<ColumnListData> {}
|
|
```
|
|
|
|
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 응답 전체 로그
|
|
- 컬럼 리스트 추출 후 로그
|
|
- 에러 상황별 상세 로그
|
|
|
|
**결과**:
|
|
|
|
- ✅ 복제 모드에서 테이블 정보 정상 로드
|
|
- ✅ 타입 안전성 향상
|
|
- ✅ 에러 핸들링 개선
|