ERP-node/테이블_복제_기능_구현_계획서.md

26 KiB

테이블 복제 기능 구현 계획서

📋 개요

테이블 타입 관리 시스템에 기존 테이블을 복제하여 새로운 테이블을 생성하는 기능을 추가합니다. 복제 시 원본 테이블의 컬럼 구조와 설정을 가져와 사용자가 수정 후 저장할 수 있도록 합니다.


🎯 주요 요구사항

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)
  • 시스템 테이블은 체크박스 비활성화
// 상태 추가
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. 최고 관리자로 로그인
  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 호출 시 두 가지 조건 모두 검증
// 백엔드 미들웨어 (기존 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 - 테이블 관리 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)

    // 추가된 타입
    export interface ColumnListData {
      columns: ColumnTypeInfo[];
      total: number;
      page: number;
      size: number;
      totalPages: number;
    }
    
    // 수정된 타입
    export interface ColumnListResponse extends ApiResponse<ColumnListData> {}
    
  2. 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];
      }
    }
    
  3. 디버그 로그 추가

    • API 응답 전체 로그
    • 컬럼 리스트 추출 후 로그
    • 에러 상황별 상세 로그

결과:

  • 복제 모드에서 테이블 정보 정상 로드
  • 타입 안전성 향상
  • 에러 핸들링 개선