/** * 컬럼 정의 테이블 컴포넌트 * 테이블 생성 시 컬럼 정의를 위한 편집 가능한 테이블 */ "use client"; import { useState } from "react"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Textarea } from "@/components/ui/textarea"; import { X, AlertCircle } from "lucide-react"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { CreateColumnDefinition, ColumnDefinitionTableProps, WEB_TYPE_OPTIONS, VALIDATION_RULES, RESERVED_WORDS, RESERVED_COLUMNS, } from "../../types/ddl"; export function ColumnDefinitionTable({ columns, onChange, disabled = false }: ColumnDefinitionTableProps) { const [validationErrors, setValidationErrors] = useState>({}); /** * 컬럼 정보 업데이트 */ const updateColumn = (index: number, updates: Partial) => { const newColumns = [...columns]; newColumns[index] = { ...newColumns[index], ...updates }; onChange(newColumns); // 업데이트 후 해당 컬럼 검증 validateColumn(index, newColumns[index]); }; /** * 컬럼 제거 */ const removeColumn = (index: number) => { if (columns.length <= 1) return; // 최소 1개 컬럼 유지 const newColumns = columns.filter((_, i) => i !== index); onChange(newColumns); // 검증 오류도 함께 제거 const newErrors = { ...validationErrors }; delete newErrors[index]; // 인덱스 재조정 const adjustedErrors: Record = {}; Object.entries(newErrors).forEach(([key, value]) => { const idx = parseInt(key); if (idx > index) { adjustedErrors[idx - 1] = value; } else if (idx < index) { adjustedErrors[idx] = value; } }); setValidationErrors(adjustedErrors); }; /** * 개별 컬럼 검증 */ const validateColumn = (index: number, column: CreateColumnDefinition) => { const errors: string[] = []; // 컬럼명 검증 if (!column.name) { errors.push("컬럼명은 필수입니다"); } else { if (!VALIDATION_RULES.columnName.pattern.test(column.name)) { errors.push(VALIDATION_RULES.columnName.errorMessage); } if ( column.name.length < VALIDATION_RULES.columnName.minLength || column.name.length > VALIDATION_RULES.columnName.maxLength ) { errors.push( `컬럼명은 ${VALIDATION_RULES.columnName.minLength}-${VALIDATION_RULES.columnName.maxLength}자여야 합니다`, ); } // 예약어 검증 if (RESERVED_WORDS.includes(column.name.toLowerCase() as any)) { errors.push("예약어는 컬럼명으로 사용할 수 없습니다"); } // 예약된 컬럼명 검증 if (RESERVED_COLUMNS.includes(column.name.toLowerCase() as any)) { errors.push("이미 자동 추가되는 기본 컬럼명입니다"); } // 중복 검증 const duplicateCount = columns.filter((col) => col.name.toLowerCase() === column.name.toLowerCase()).length; if (duplicateCount > 1) { errors.push("중복된 컬럼명입니다"); } } // 웹타입 검증 if (!column.webType) { errors.push("웹타입을 선택해주세요"); } // 길이 검증 (길이를 지원하는 타입인 경우) const webTypeOption = WEB_TYPE_OPTIONS.find((opt) => opt.value === column.webType); if (webTypeOption?.supportsLength && column.length !== undefined) { if (column.length < VALIDATION_RULES.columnLength.min || column.length > VALIDATION_RULES.columnLength.max) { errors.push(VALIDATION_RULES.columnLength.errorMessage); } } // 검증 오류 상태 업데이트 setValidationErrors((prev) => ({ ...prev, [index]: errors, })); return errors.length === 0; }; /** * 웹타입 변경 시 길이 기본값 설정 */ const handleWebTypeChange = (index: number, webType: string) => { const webTypeOption = WEB_TYPE_OPTIONS.find((opt) => opt.value === webType); const updates: Partial = { webType: webType as any }; // 길이를 지원하는 타입이고 현재 길이가 없으면 기본값 설정 if (webTypeOption?.supportsLength && !columns[index].length && webTypeOption.defaultLength) { updates.length = webTypeOption.defaultLength; } // 길이를 지원하지 않는 타입이면 길이 제거 if (!webTypeOption?.supportsLength) { updates.length = undefined; } updateColumn(index, updates); }; /** * 전체 검증 상태 확인 */ const hasValidationErrors = Object.values(validationErrors).some((errors) => errors.length > 0); return (
{/* 검증 오류 요약 */} {hasValidationErrors && ( 컬럼 정의에 오류가 있습니다. 각 컬럼의 오류를 확인해주세요. )} {/* 컬럼 정의 테이블 */}
컬럼명 * 라벨 웹타입 * 필수 길이 기본값 설명 {columns.map((column, index) => { const webTypeOption = WEB_TYPE_OPTIONS.find((opt) => opt.value === column.webType); const rowErrors = validationErrors[index] || []; const hasRowError = rowErrors.length > 0; return (
updateColumn(index, { name: e.target.value })} placeholder="column_name" disabled={disabled} className={hasRowError ? "border-red-300" : ""} /> {rowErrors.length > 0 && (
{rowErrors.map((error, i) => (
{error}
))}
)}
updateColumn(index, { label: e.target.value })} placeholder="컬럼 라벨" disabled={disabled} />
updateColumn(index, { nullable: !checked })} disabled={disabled} />
updateColumn(index, { length: e.target.value ? parseInt(e.target.value) : undefined, }) } placeholder={webTypeOption?.defaultLength?.toString() || ""} disabled={disabled || !webTypeOption?.supportsLength} min={1} max={65535} /> updateColumn(index, { defaultValue: e.target.value })} placeholder="기본값" disabled={disabled} />