Files
vexplor/테이블_동적_생성_기능_개발_계획서.md
2025-09-22 17:00:59 +09:00

31 KiB
Raw Blame History

📊 테이블 동적 생성 기능 개발 계획서

📋 프로젝트 개요

🎯 목적

현재 테이블 타입 관리 시스템에 실제 데이터베이스 테이블과 컬럼을 생성하는 기능을 추가하여, 최고 관리자가 동적으로 새로운 테이블을 생성하고 기존 테이블에 컬럼을 추가할 수 있는 시스템을 구축합니다.

🔐 핵심 보안 요구사항

  • 최고 관리자 전용: 회사코드가 *인 사용자만 DDL 실행 가능
  • 시스템 테이블 보호: 핵심 시스템 테이블 수정 금지
  • SQL 인젝션 방지: 모든 입력값에 대한 엄격한 검증
  • 트랜잭션 안전성: DDL 실행과 메타데이터 저장의 원자성 보장

🔍 현재 시스템 분석

기존 기능

  • 테이블 조회: information_schema를 통한 기존 테이블 스캔
  • 컬럼 관리: 컬럼별 웹타입 설정 및 메타데이터 관리
  • 데이터 CRUD: 기존 테이블의 데이터 조작
  • 권한 관리: 회사별 데이터 접근 제어

누락 기능

  • 실제 테이블 생성: DDL CREATE TABLE 실행
  • 컬럼 추가: DDL ALTER TABLE ADD COLUMN 실행
  • 스키마 변경: 데이터베이스 구조 변경

🚀 개발 단계별 계획

📦 Phase 1: 권한 시스템 강화 (2일)

1.1 슈퍼관리자 미들웨어 구현

// 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 실행 서비스 클래스

// 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 타입으로 변환
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 컨트롤러

// 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 라우팅 설정

// 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 테이블 생성 모달

// 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 컬럼 정의 테이블 컴포넌트

// 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 컬럼 추가 모달

// 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 테이블 관리 페이지 확장

// 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 안전성 검증기

// 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 실행 로깅

// 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 테스트 시나리오

// 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 관련 타입 정의

데이터베이스 스키마 추가

-- 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% 이상
  • 모든 테스트 케이스 통과
  • 보안 검증 완료
  • 성능 테스트 통과

📝 참고사항

🔗 관련 문서

⚠️ 주의사항

  1. 시스템 테이블 보호: 핵심 시스템 테이블은 절대 수정하지 않음
  2. 백업 필수: DDL 실행 전 데이터베이스 백업 권장
  3. 점진적 배포: 개발환경 → 스테이징 → 운영환경 순차 배포
  4. 롤백 계획: 문제 발생 시 즉시 롤백할 수 있는 계획 수립

이 계획서를 통해 안전하고 강력한 테이블 동적 생성 기능을 체계적으로 개발할 수 있습니다. 🚀