Files
vexplor/테이블_동적_생성_기능_개발_계획서.md

1091 lines
31 KiB
Markdown
Raw Permalink Normal View History

# 📊 테이블 동적 생성 기능 개발 계획서
## 📋 프로젝트 개요
### 🎯 목적
현재 테이블 타입 관리 시스템에 **실제 데이터베이스 테이블과 컬럼을 생성**하는 기능을 추가하여, 최고 관리자가 동적으로 새로운 테이블을 생성하고 기존 테이블에 컬럼을 추가할 수 있는 시스템을 구축합니다.
### 🔐 핵심 보안 요구사항
- **최고 관리자 전용**: 회사코드가 `*`인 사용자만 DDL 실행 가능
- **시스템 테이블 보호**: 핵심 시스템 테이블 수정 금지
- **SQL 인젝션 방지**: 모든 입력값에 대한 엄격한 검증
- **트랜잭션 안전성**: DDL 실행과 메타데이터 저장의 원자성 보장
---
## 🔍 현재 시스템 분석
### ✅ 기존 기능
- **테이블 조회**: `information_schema`를 통한 기존 테이블 스캔
- **컬럼 관리**: 컬럼별 웹타입 설정 및 메타데이터 관리
- **데이터 CRUD**: 기존 테이블의 데이터 조작
- **권한 관리**: 회사별 데이터 접근 제어
### ❌ 누락 기능
- **실제 테이블 생성**: DDL `CREATE TABLE` 실행
- **컬럼 추가**: DDL `ALTER TABLE ADD COLUMN` 실행
- **스키마 변경**: 데이터베이스 구조 변경
---
## 🚀 개발 단계별 계획
### 📦 Phase 1: 권한 시스템 강화 (2일)
#### 1.1 슈퍼관리자 미들웨어 구현
```typescript
// backend-node/src/middleware/superAdminMiddleware.ts
export const requireSuperAdmin = (
req: AuthenticatedRequest,
res: Response,
next: NextFunction
): void => {
if (!req.user || req.user.companyCode !== "*") {
return res.status(403).json({
success: false,
error: {
code: "SUPER_ADMIN_REQUIRED",
details: "최고 관리자 권한이 필요합니다.",
},
});
}
next();
};
```
#### 1.2 권한 검증 로직 확장
- 사용자 세션 유효성 확인
- DDL 실행 권한 이중 검증
- 로그 기록 및 감사 추적
### 📦 Phase 2: DDL 실행 서비스 구현 (3일)
#### 2.1 DDL 실행 서비스 클래스
```typescript
// backend-node/src/services/ddlExecutionService.ts
export class DDLExecutionService {
/**
* 새 테이블 생성
*/
async createTable(
tableName: string,
columns: CreateColumnDefinition[],
userCompanyCode: string
): Promise<void> {
// 권한 검증
this.validateSuperAdminPermission(userCompanyCode);
// 테이블명 검증
this.validateTableName(tableName);
// DDL 쿼리 생성 및 실행
const ddlQuery = this.generateCreateTableQuery(tableName, columns);
await prisma.$transaction(async (tx) => {
// 1. 테이블 생성
await tx.$executeRawUnsafe(ddlQuery);
// 2. 메타데이터 저장
await this.saveTableMetadata(tx, tableName, columns);
});
}
/**
* 기존 테이블에 컬럼 추가
*/
async addColumn(
tableName: string,
column: CreateColumnDefinition,
userCompanyCode: string
): Promise<void> {
// 유사한 구조로 구현
}
}
```
#### 2.2 DDL 쿼리 생성기
- **CREATE TABLE**: 기본 컬럼(id, created_date, updated_date, company_code) 자동 포함
- **ALTER TABLE**: 안전한 컬럼 추가
- **타입 매핑**: 웹타입을 PostgreSQL 타입으로 변환
```typescript
private mapWebTypeToPostgresType(webType: string, length?: number): string {
const typeMap: Record<string, string> = {
'text': length ? `varchar(${length})` : 'text',
'number': 'integer',
'decimal': 'numeric(10,2)',
'date': 'date',
'datetime': 'timestamp',
'boolean': 'boolean',
'code': 'varchar(100)',
'entity': 'integer',
'file': 'text',
'email': 'varchar(255)'
};
return typeMap[webType] || 'text';
}
```
### 📦 Phase 3: API 엔드포인트 구현 (2일)
#### 3.1 DDL 컨트롤러
```typescript
// backend-node/src/controllers/ddlController.ts
export class DDLController {
/**
* POST /api/ddl/tables - 새 테이블 생성
*/
static async createTable(req: AuthenticatedRequest, res: Response) {
try {
const { tableName, columns, description } = req.body;
const userCompanyCode = req.user?.companyCode;
const ddlService = new DDLExecutionService();
await ddlService.createTable(tableName, columns, userCompanyCode);
res.json({
success: true,
message: `테이블 '${tableName}'이 성공적으로 생성되었습니다.`,
data: { tableName },
});
} catch (error) {
logger.error("테이블 생성 실패:", error);
res.status(400).json({
success: false,
error: { code: "TABLE_CREATION_FAILED", details: error.message },
});
}
}
/**
* POST /api/ddl/tables/:tableName/columns - 컬럼 추가
*/
static async addColumn(req: AuthenticatedRequest, res: Response) {
// 컬럼 추가 로직
}
}
```
#### 3.2 라우팅 설정
```typescript
// backend-node/src/routes/ddlRoutes.ts
import { requireSuperAdmin } from "../middleware/superAdminMiddleware";
const router = express.Router();
router.post("/tables", requireSuperAdmin, DDLController.createTable);
router.post(
"/tables/:tableName/columns",
requireSuperAdmin,
DDLController.addColumn
);
export default router;
```
### 📦 Phase 4: 프론트엔드 UI 구현 (3일)
#### 4.1 테이블 생성 모달
```tsx
// frontend/components/admin/CreateTableModal.tsx
export function CreateTableModal({ isOpen, onClose, onSuccess }: Props) {
const [tableName, setTableName] = useState("");
const [description, setDescription] = useState("");
const [columns, setColumns] = useState<CreateColumnDefinition[]>([
{ name: "name", label: "이름", webType: "text", nullable: false },
]);
const handleCreateTable = async () => {
try {
await apiClient.post("/ddl/tables", {
tableName,
description,
columns,
});
toast.success(`테이블 '${tableName}'이 생성되었습니다.`);
onSuccess();
onClose();
} catch (error) {
toast.error("테이블 생성에 실패했습니다.");
}
};
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-4xl">
<DialogHeader>
<DialogTitle>🆕 새 테이블 생성</DialogTitle>
<DialogDescription>
최고 관리자만 새로운 테이블을 생성할 수 있습니다.
</DialogDescription>
</DialogHeader>
<div className="space-y-6">
{/* 테이블 기본 정보 */}
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="tableName">테이블명 *</Label>
<Input
id="tableName"
value={tableName}
onChange={(e) => setTableName(e.target.value)}
placeholder="예: customer_info"
pattern="^[a-zA-Z_][a-zA-Z0-9_]*$"
/>
<p className="text-sm text-muted-foreground mt-1">
영문자, 숫자, 언더스코어만 사용 가능
</p>
</div>
<div>
<Label htmlFor="description">설명</Label>
<Input
id="description"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="테이블 설명"
/>
</div>
</div>
{/* 컬럼 정의 테이블 */}
<div>
<div className="flex items-center justify-between mb-3">
<Label>컬럼 정의 *</Label>
<Button
type="button"
variant="outline"
size="sm"
onClick={() =>
setColumns([
...columns,
{
name: "",
label: "",
webType: "text",
nullable: true,
order: columns.length + 1,
},
])
}
>
+ 컬럼 추가
</Button>
</div>
<ColumnDefinitionTable columns={columns} onChange={setColumns} />
</div>
{/* 기본 컬럼 안내 */}
<Alert>
<InfoIcon className="h-4 w-4" />
<AlertTitle>자동 추가 컬럼</AlertTitle>
<AlertDescription>
다음 컬럼들이 자동으로 추가됩니다:
<code>id</code>(PK), <code>created_date</code>,<code>
updated_date
</code>, <code>company_code</code>
</AlertDescription>
</Alert>
</div>
<DialogFooter>
<Button variant="outline" onClick={onClose}>
취소
</Button>
<Button
onClick={handleCreateTable}
disabled={!tableName || columns.length === 0}
>
테이블 생성
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
```
#### 4.2 컬럼 정의 테이블 컴포넌트
```tsx
// frontend/components/admin/ColumnDefinitionTable.tsx
export function ColumnDefinitionTable({
columns,
onChange,
}: ColumnDefinitionTableProps) {
const updateColumn = (
index: number,
updates: Partial<CreateColumnDefinition>
) => {
const newColumns = [...columns];
newColumns[index] = { ...newColumns[index], ...updates };
onChange(newColumns);
};
const removeColumn = (index: number) => {
const newColumns = columns.filter((_, i) => i !== index);
onChange(newColumns);
};
return (
<div className="border rounded-lg">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[150px]">컬럼명</TableHead>
<TableHead className="w-[150px]">라벨</TableHead>
<TableHead className="w-[120px]">웹타입</TableHead>
<TableHead className="w-[100px]">필수</TableHead>
<TableHead className="w-[100px]">길이</TableHead>
<TableHead>설명</TableHead>
<TableHead className="w-[50px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{columns.map((column, index) => (
<TableRow key={index}>
<TableCell>
<Input
value={column.name}
onChange={(e) =>
updateColumn(index, { name: e.target.value })
}
placeholder="column_name"
pattern="^[a-zA-Z_][a-zA-Z0-9_]*$"
/>
</TableCell>
<TableCell>
<Input
value={column.label || ""}
onChange={(e) =>
updateColumn(index, { label: e.target.value })
}
placeholder="컬럼 라벨"
/>
</TableCell>
<TableCell>
<Select
value={column.webType}
onValueChange={(value) =>
updateColumn(index, { webType: value })
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="text">텍스트</SelectItem>
<SelectItem value="number">숫자</SelectItem>
<SelectItem value="decimal">소수</SelectItem>
<SelectItem value="date">날짜</SelectItem>
<SelectItem value="datetime">날짜시간</SelectItem>
<SelectItem value="boolean">불린</SelectItem>
<SelectItem value="code">코드</SelectItem>
<SelectItem value="entity">엔티티</SelectItem>
<SelectItem value="file">파일</SelectItem>
</SelectContent>
</Select>
</TableCell>
<TableCell>
<Checkbox
checked={!column.nullable}
onCheckedChange={(checked) =>
updateColumn(index, { nullable: !checked })
}
/>
</TableCell>
<TableCell>
<Input
type="number"
value={column.length || ""}
onChange={(e) =>
updateColumn(index, {
length: e.target.value
? parseInt(e.target.value)
: undefined,
})
}
placeholder="길이"
disabled={!["text", "code"].includes(column.webType)}
/>
</TableCell>
<TableCell>
<Input
value={column.description || ""}
onChange={(e) =>
updateColumn(index, { description: e.target.value })
}
placeholder="컬럼 설명"
/>
</TableCell>
<TableCell>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => removeColumn(index)}
disabled={columns.length === 1}
>
<X className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
}
```
#### 4.3 컬럼 추가 모달
```tsx
// frontend/components/admin/AddColumnModal.tsx
export function AddColumnModal({
isOpen,
onClose,
tableName,
onSuccess,
}: AddColumnModalProps) {
const [column, setColumn] = useState<CreateColumnDefinition>({
name: "",
label: "",
webType: "text",
nullable: true,
order: 0,
});
const handleAddColumn = async () => {
try {
await apiClient.post(`/ddl/tables/${tableName}/columns`, { column });
toast.success(`컬럼 '${column.name}'이 추가되었습니다.`);
onSuccess();
onClose();
} catch (error) {
toast.error("컬럼 추가에 실패했습니다.");
}
};
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent>
<DialogHeader>
<DialogTitle> 컬럼 추가 - {tableName}</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label>컬럼명 *</Label>
<Input
value={column.name}
onChange={(e) => setColumn({ ...column, name: e.target.value })}
placeholder="column_name"
pattern="^[a-zA-Z_][a-zA-Z0-9_]*$"
/>
</div>
<div>
<Label>라벨</Label>
<Input
value={column.label || ""}
onChange={(e) =>
setColumn({ ...column, label: e.target.value })
}
placeholder="컬럼 라벨"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label>웹타입 *</Label>
<Select
value={column.webType}
onValueChange={(value) =>
setColumn({ ...column, webType: value })
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="text">텍스트</SelectItem>
<SelectItem value="number">숫자</SelectItem>
<SelectItem value="date">날짜</SelectItem>
<SelectItem value="boolean">불린</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label>길이</Label>
<Input
type="number"
value={column.length || ""}
onChange={(e) =>
setColumn({
...column,
length: e.target.value
? parseInt(e.target.value)
: undefined,
})
}
disabled={!["text", "code"].includes(column.webType)}
/>
</div>
</div>
<div>
<div className="flex items-center space-x-2">
<Checkbox
id="nullable"
checked={!column.nullable}
onCheckedChange={(checked) =>
setColumn({ ...column, nullable: !checked })
}
/>
<Label htmlFor="nullable">필수 입력</Label>
</div>
</div>
<div>
<Label>설명</Label>
<Textarea
value={column.description || ""}
onChange={(e) =>
setColumn({ ...column, description: e.target.value })
}
placeholder="컬럼 설명"
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={onClose}>
취소
</Button>
<Button onClick={handleAddColumn} disabled={!column.name}>
컬럼 추가
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
```
#### 4.4 테이블 관리 페이지 확장
```tsx
// frontend/app/(main)/admin/tableMng/page.tsx (기존 페이지 확장)
export default function TableManagementPage() {
const [createTableModalOpen, setCreateTableModalOpen] = useState(false);
const [addColumnModalOpen, setAddColumnModalOpen] = useState(false);
const { user } = useAuth();
// 최고 관리자 여부 확인
const isSuperAdmin = user?.companyCode === "*";
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold">테이블 타입 관리</h1>
{isSuperAdmin && (
<div className="flex gap-2">
<Button
onClick={() => setCreateTableModalOpen(true)}
className="bg-green-600 hover:bg-green-700"
>
<Plus className="h-4 w-4 mr-2" />새 테이블 생성
</Button>
{selectedTable && (
<Button
onClick={() => setAddColumnModalOpen(true)}
variant="outline"
>
<Plus className="h-4 w-4 mr-2" />
컬럼 추가
</Button>
)}
</div>
)}
</div>
{/* 기존 테이블 목록 및 컬럼 관리 UI */}
{/* 새 테이블 생성 모달 */}
<CreateTableModal
isOpen={createTableModalOpen}
onClose={() => setCreateTableModalOpen(false)}
onSuccess={() => {
loadTables();
setCreateTableModalOpen(false);
}}
/>
{/* 컬럼 추가 모달 */}
<AddColumnModal
isOpen={addColumnModalOpen}
onClose={() => setAddColumnModalOpen(false)}
tableName={selectedTable || ""}
onSuccess={() => {
loadColumns(selectedTable!);
setAddColumnModalOpen(false);
}}
/>
</div>
);
}
```
### 📦 Phase 5: 안전성 검증 시스템 (2일)
#### 5.1 DDL 안전성 검증기
```typescript
// backend-node/src/services/ddlSafetyValidator.ts
export class DDLSafetyValidator {
/**
* 테이블 생성 전 검증
*/
static validateTableCreation(
tableName: string,
columns: CreateColumnDefinition[]
): ValidationResult {
const errors: string[] = [];
// 1. 테이블명 검증
if (!this.isValidTableName(tableName)) {
errors.push(
"유효하지 않은 테이블명입니다. 영문자로 시작하고 영문자, 숫자, 언더스코어만 사용 가능합니다."
);
}
// 2. 시스템 테이블 보호
if (this.isSystemTable(tableName)) {
errors.push(`'${tableName}'은 시스템 테이블명으로 사용할 수 없습니다.`);
}
// 3. 예약어 검증
if (this.isReservedWord(tableName)) {
errors.push(`'${tableName}'은 예약어이므로 사용할 수 없습니다.`);
}
// 4. 길이 검증
if (tableName.length > 63) {
errors.push("테이블명은 63자를 초과할 수 없습니다.");
}
// 5. 컬럼 검증
if (columns.length === 0) {
errors.push("최소 1개의 컬럼이 필요합니다.");
}
// 6. 컬럼명 중복 검증
const columnNames = columns.map((col) => col.name.toLowerCase());
const duplicates = columnNames.filter(
(name, index) => columnNames.indexOf(name) !== index
);
if (duplicates.length > 0) {
errors.push(`중복된 컬럼명: ${duplicates.join(", ")}`);
}
// 7. 컬럼명 검증
for (const column of columns) {
if (!this.isValidColumnName(column.name)) {
errors.push(`유효하지 않은 컬럼명: ${column.name}`);
}
if (this.isReservedColumnName(column.name)) {
errors.push(`'${column.name}'은 예약된 컬럼명입니다.`);
}
}
return {
isValid: errors.length === 0,
errors,
};
}
/**
* 컬럼 추가 전 검증
*/
static validateColumnAddition(
tableName: string,
column: CreateColumnDefinition
): ValidationResult {
const errors: string[] = [];
// 컬럼명 검증
if (!this.isValidColumnName(column.name)) {
errors.push("유효하지 않은 컬럼명입니다.");
}
// 예약된 컬럼명 검증
if (this.isReservedColumnName(column.name)) {
errors.push(`'${column.name}'은 예약된 컬럼명입니다.`);
}
return {
isValid: errors.length === 0,
errors,
};
}
/**
* 테이블명 유효성 검증
*/
private static isValidTableName(tableName: string): boolean {
const tableNameRegex = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
return tableNameRegex.test(tableName);
}
/**
* 컬럼명 유효성 검증
*/
private static isValidColumnName(columnName: string): boolean {
const columnNameRegex = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
return columnNameRegex.test(columnName) && columnName.length <= 63;
}
/**
* 시스템 테이블 확인
*/
private static isSystemTable(tableName: string): boolean {
const systemTables = [
"user_info",
"company_mng",
"menu_info",
"auth_group",
"table_labels",
"column_labels",
"screen_definitions",
"screen_layouts",
"common_code",
"multi_lang_key_master",
"multi_lang_text",
"button_action_standards",
];
return systemTables.includes(tableName.toLowerCase());
}
/**
* 예약어 확인
*/
private static isReservedWord(word: string): boolean {
const reservedWords = [
"user",
"order",
"group",
"table",
"column",
"index",
"select",
"insert",
"update",
"delete",
"from",
"where",
"join",
"on",
"as",
"and",
"or",
"not",
"null",
"true",
"false",
];
return reservedWords.includes(word.toLowerCase());
}
/**
* 예약된 컬럼명 확인
*/
private static isReservedColumnName(columnName: string): boolean {
const reservedColumns = [
"id",
"created_date",
"updated_date",
"company_code",
];
return reservedColumns.includes(columnName.toLowerCase());
}
}
```
#### 5.2 DDL 실행 로깅
```typescript
// backend-node/src/services/ddlAuditLogger.ts
export class DDLAuditLogger {
/**
* DDL 실행 로그 기록
*/
static async logDDLExecution(
userId: string,
companyCode: string,
ddlType: "CREATE_TABLE" | "ADD_COLUMN",
tableName: string,
ddlQuery: string,
success: boolean,
error?: string
): Promise<void> {
try {
await prisma.ddl_execution_log.create({
data: {
user_id: userId,
company_code: companyCode,
ddl_type: ddlType,
table_name: tableName,
ddl_query: ddlQuery,
success: success,
error_message: error,
executed_at: new Date(),
},
});
} catch (logError) {
logger.error("DDL 실행 로그 기록 실패:", logError);
}
}
}
```
### 📦 Phase 6: 통합 테스트 및 검증 (2일)
#### 6.1 테스트 시나리오
```typescript
// backend-node/src/test/ddl-execution.test.ts
describe("DDL 실행 테스트", () => {
test("최고 관리자 - 테이블 생성 성공", async () => {
const response = await request(app)
.post("/api/ddl/tables")
.set("Authorization", `Bearer ${superAdminToken}`)
.send({
tableName: "test_table",
columns: [
{ name: "name", webType: "text", nullable: false },
{ name: "email", webType: "email", nullable: true },
],
});
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
});
test("일반 사용자 - 테이블 생성 권한 거부", async () => {
const response = await request(app)
.post("/api/ddl/tables")
.set("Authorization", `Bearer ${normalUserToken}`)
.send({
tableName: "test_table",
columns: [{ name: "name", webType: "text" }],
});
expect(response.status).toBe(403);
expect(response.body.error.code).toBe("SUPER_ADMIN_REQUIRED");
});
test("유효하지 않은 테이블명 - 생성 실패", async () => {
const response = await request(app)
.post("/api/ddl/tables")
.set("Authorization", `Bearer ${superAdminToken}`)
.send({
tableName: "123invalid",
columns: [{ name: "name", webType: "text" }],
});
expect(response.status).toBe(400);
expect(response.body.error.code).toBe("VALIDATION_FAILED");
});
test("시스템 테이블명 사용 - 생성 실패", async () => {
const response = await request(app)
.post("/api/ddl/tables")
.set("Authorization", `Bearer ${superAdminToken}`)
.send({
tableName: "user_info",
columns: [{ name: "name", webType: "text" }],
});
expect(response.status).toBe(400);
expect(response.body.error.details).toContain("시스템 테이블명");
});
});
```
#### 6.2 통합 테스트 체크리스트
- [ ] **권한 테스트**: 최고 관리자만 DDL 실행 가능
- [ ] **테이블 생성 테스트**: 다양한 웹타입으로 테이블 생성
- [ ] **컬럼 추가 테스트**: 기존 테이블에 컬럼 추가
- [ ] **메타데이터 동기화**: DDL 실행 후 메타데이터 정확성
- [ ] **오류 처리 테스트**: 잘못된 입력값 처리
- [ ] **트랜잭션 테스트**: 실패 시 롤백 확인
- [ ] **로깅 테스트**: DDL 실행 로그 기록 확인
---
## 🔒 보안 및 안전성 고려사항
### 🛡️ 보안 검증
1. **다층 권한 검증**
- 미들웨어 레벨 권한 체크
- 서비스 레벨 추가 검증
- 사용자 세션 유효성 확인
2. **SQL 인젝션 방지**
- 테이블명/컬럼명 정규식 검증
- 화이트리스트 기반 검증
- 파라미터 바인딩 사용
3. **시스템 보호**
- 핵심 시스템 테이블 수정 금지
- 예약어 사용 금지
- 테이블명/컬럼명 길이 제한
### 🔄 트랜잭션 안전성
- DDL 실행과 메타데이터 저장을 하나의 트랜잭션으로 처리
- 실패 시 자동 롤백
- 상세한 로그 기록 및 모니터링
### 📊 모니터링 및 감사
- 모든 DDL 실행 로그 기록
- 실행 시간 및 성공/실패 추적
- 정기적인 시스템 상태 점검
---
## 📅 개발 일정
| Phase | 작업 내용 | 소요 기간 | 담당자 |
| ------- | -------------------- | --------- | --------------- |
| Phase 1 | 권한 시스템 강화 | 2일 | Backend 개발자 |
| Phase 2 | DDL 실행 서비스 구현 | 3일 | Backend 개발자 |
| Phase 3 | API 엔드포인트 구현 | 2일 | Backend 개발자 |
| Phase 4 | 프론트엔드 UI 구현 | 3일 | Frontend 개발자 |
| Phase 5 | 안전성 검증 시스템 | 2일 | Backend 개발자 |
| Phase 6 | 통합 테스트 및 검증 | 2일 | 전체 팀 |
**총 개발 기간: 14일 (약 3주)**
---
## 📂 파일 구조 변경사항
### 백엔드 추가 파일
```
backend-node/src/
├── middleware/
│ └── superAdminMiddleware.ts # 최고 관리자 권한 미들웨어
├── services/
│ ├── ddlExecutionService.ts # DDL 실행 서비스
│ ├── ddlSafetyValidator.ts # DDL 안전성 검증기
│ └── ddlAuditLogger.ts # DDL 실행 로깅
├── controllers/
│ └── ddlController.ts # DDL 실행 컨트롤러
├── routes/
│ └── ddlRoutes.ts # DDL API 라우터
├── types/
│ └── ddl.ts # DDL 관련 타입 정의
└── test/
└── ddl-execution.test.ts # DDL 실행 테스트
```
### 프론트엔드 추가 파일
```
frontend/
├── components/admin/
│ ├── CreateTableModal.tsx # 테이블 생성 모달
│ ├── AddColumnModal.tsx # 컬럼 추가 모달
│ └── ColumnDefinitionTable.tsx # 컬럼 정의 테이블
├── lib/api/
│ └── ddl.ts # DDL API 클라이언트
└── types/
└── ddl.ts # DDL 관련 타입 정의
```
### 데이터베이스 스키마 추가
```sql
-- DDL 실행 로그 테이블
CREATE TABLE ddl_execution_log (
id serial PRIMARY KEY,
user_id varchar(100) NOT NULL,
company_code varchar(50) NOT NULL,
ddl_type varchar(50) NOT NULL,
table_name varchar(100) NOT NULL,
ddl_query text NOT NULL,
success boolean NOT NULL,
error_message text,
executed_at timestamp DEFAULT now()
);
```
---
## 🎯 성공 기준
### ✅ 기능적 요구사항
- [ ] 최고 관리자만 테이블/컬럼 생성 가능
- [ ] PostgreSQL 테이블 실제 생성 및 컬럼 추가
- [ ] 메타데이터 자동 동기화
- [ ] 사용자 친화적인 UI 제공
### ✅ 비기능적 요구사항
- [ ] 모든 DDL 실행 로깅
- [ ] SQL 인젝션 방지
- [ ] 트랜잭션 안전성 보장
- [ ] 시스템 테이블 보호
### ✅ 품질 기준
- [ ] 코드 커버리지 90% 이상
- [ ] 모든 테스트 케이스 통과
- [ ] 보안 검증 완료
- [ ] 성능 테스트 통과
---
## 📝 참고사항
### 🔗 관련 문서
- [현재 테이블 타입 관리 시스템](<frontend/app/(main)/admin/tableMng/page.tsx>)
- [기존 권한 시스템](backend-node/src/middleware/authMiddleware.ts)
- [DDL 실행 가이드](docs/NodeJS_Refactoring_Rules.md)
### ⚠️ 주의사항
1. **시스템 테이블 보호**: 핵심 시스템 테이블은 절대 수정하지 않음
2. **백업 필수**: DDL 실행 전 데이터베이스 백업 권장
3. **점진적 배포**: 개발환경 → 스테이징 → 운영환경 순차 배포
4. **롤백 계획**: 문제 발생 시 즉시 롤백할 수 있는 계획 수립
이 계획서를 통해 안전하고 강력한 테이블 동적 생성 기능을 체계적으로 개발할 수 있습니다. 🚀