ERP-node/테이블_동적_생성_기능_개발_계획서.md

1091 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 📊 테이블 동적 생성 기능 개발 계획서
## 📋 프로젝트 개요
### 🎯 목적
현재 테이블 타입 관리 시스템에 **실제 데이터베이스 테이블과 컬럼을 생성**하는 기능을 추가하여, 최고 관리자가 동적으로 새로운 테이블을 생성하고 기존 테이블에 컬럼을 추가할 수 있는 시스템을 구축합니다.
### 🔐 핵심 보안 요구사항
- **최고 관리자 전용**: 회사코드가 `*`인 사용자만 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. **롤백 계획**: 문제 발생 시 즉시 롤백할 수 있는 계획 수립
이 계획서를 통해 안전하고 강력한 테이블 동적 생성 기능을 체계적으로 개발할 수 있습니다. 🚀