/** * 컬럼 추가 모달 컴포넌트 * 기존 테이블에 새로운 컬럼을 추가하기 위한 모달 */ "use client"; import { useState, useEffect } from "react"; import { ResizableDialog, ResizableDialogContent, ResizableDialogHeader, ResizableDialogTitle, ResizableDialogDescription, ResizableDialogFooter, } from "@/components/ui/resizable-dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Checkbox } from "@/components/ui/checkbox"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Loader2, Plus, AlertCircle } from "lucide-react"; import { toast } from "sonner"; import { ddlApi } from "../../lib/api/ddl"; import { AddColumnModalProps, CreateColumnDefinition, VALIDATION_RULES, RESERVED_WORDS, RESERVED_COLUMNS, } from "../../types/ddl"; import { INPUT_TYPE_OPTIONS } from "../../types/input-types"; export function AddColumnModal({ isOpen, onClose, tableName, onSuccess }: AddColumnModalProps) { const [column, setColumn] = useState({ name: "", label: "", inputType: "text", nullable: true, order: 0, }); const [loading, setLoading] = useState(false); const [validationErrors, setValidationErrors] = useState([]); /** * 모달 리셋 */ const resetModal = () => { setColumn({ name: "", label: "", inputType: "text", nullable: true, order: 0, }); setValidationErrors([]); }; /** * 모달 열림/닫힘 시 리셋 */ useEffect(() => { if (isOpen) { resetModal(); } }, [isOpen]); /** * 컬럼 정보 업데이트 */ const updateColumn = (updates: Partial) => { const newColumn = { ...column, ...updates }; setColumn(newColumn); // 업데이트 후 검증 validateColumn(newColumn); }; /** * 컬럼 검증 */ const validateColumn = (columnData: CreateColumnDefinition) => { const errors: string[] = []; // 컬럼명 검증 if (!columnData.name) { errors.push("컬럼명은 필수입니다."); } else { if (!VALIDATION_RULES.columnName.pattern.test(columnData.name)) { errors.push(VALIDATION_RULES.columnName.errorMessage); } if ( columnData.name.length < VALIDATION_RULES.columnName.minLength || columnData.name.length > VALIDATION_RULES.columnName.maxLength ) { errors.push( `컬럼명은 ${VALIDATION_RULES.columnName.minLength}-${VALIDATION_RULES.columnName.maxLength}자여야 합니다.`, ); } // 예약어 검증 if (RESERVED_WORDS.includes(columnData.name.toLowerCase() as any)) { errors.push("SQL 예약어는 컬럼명으로 사용할 수 없습니다."); } // 예약된 컬럼명 검증 if (RESERVED_COLUMNS.includes(columnData.name.toLowerCase() as any)) { errors.push("이미 자동 추가되는 기본 컬럼명입니다."); } // 네이밍 컨벤션 검증 if (columnData.name.startsWith("_") || columnData.name.endsWith("_")) { errors.push("컬럼명은 언더스코어로 시작하거나 끝날 수 없습니다."); } if (columnData.name.includes("__")) { errors.push("컬럼명에 연속된 언더스코어는 사용할 수 없습니다."); } } // 입력타입 검증 if (!columnData.inputType) { errors.push("입력타입을 선택해주세요."); } // 길이 검증 (길이를 지원하는 타입인 경우) const inputTypeOption = INPUT_TYPE_OPTIONS.find((opt) => opt.value === columnData.inputType); if (inputTypeOption?.supportsLength && columnData.length !== undefined) { if ( columnData.length < VALIDATION_RULES.columnLength.min || columnData.length > VALIDATION_RULES.columnLength.max ) { errors.push(VALIDATION_RULES.columnLength.errorMessage); } } setValidationErrors(errors); return errors.length === 0; }; /** * 입력타입 변경 처리 */ const handleInputTypeChange = (inputType: string) => { const inputTypeOption = INPUT_TYPE_OPTIONS.find((opt) => opt.value === inputType); const updates: Partial = { inputType: inputType as any }; // 길이를 지원하는 타입이고 현재 길이가 없으면 기본값 설정 if (inputTypeOption?.supportsLength && !column.length && inputTypeOption.defaultLength) { updates.length = inputTypeOption.defaultLength; } // 길이를 지원하지 않는 타입이면 길이 제거 if (!inputTypeOption?.supportsLength) { updates.length = undefined; } updateColumn(updates); }; /** * 컬럼 추가 실행 */ const handleAddColumn = async () => { if (!validateColumn(column)) { toast.error("입력값을 확인해주세요."); return; } setLoading(true); try { const result = await ddlApi.addColumn(tableName, { column }); if (result.success) { toast.success(result.message); onSuccess(result); onClose(); } else { toast.error(result.error?.details || result.message); } } catch (error: any) { // console.error("컬럼 추가 실패:", error); toast.error(error.response?.data?.error?.details || "컬럼 추가에 실패했습니다."); } finally { setLoading(false); } }; /** * 폼 유효성 확인 */ const isFormValid = validationErrors.length === 0 && column.name && column.inputType; const inputTypeOption = INPUT_TYPE_OPTIONS.find((opt) => opt.value === column.inputType); return ( 컬럼 추가 - {tableName}
{/* 검증 오류 표시 */} {validationErrors.length > 0 && (
{validationErrors.map((error, index) => (
• {error}
))}
)} {/* 기본 정보 */}
updateColumn({ name: e.target.value })} placeholder="column_name" disabled={loading} className={validationErrors.some((e) => e.includes("컬럼명")) ? "border-red-300" : ""} />

영문자로 시작, 영문자/숫자/언더스코어만 사용 가능

updateColumn({ label: e.target.value })} placeholder="컬럼 라벨" disabled={loading} />

화면에 표시될 라벨 (선택사항)

{/* 타입 및 속성 */}
updateColumn({ length: e.target.value ? parseInt(e.target.value) : undefined, }) } placeholder={inputTypeOption?.defaultLength?.toString() || ""} disabled={loading || !inputTypeOption?.supportsLength} min={1} max={65535} />

{inputTypeOption?.supportsLength ? "1-65535 범위에서 설정 가능" : "이 타입은 길이 설정이 불가능합니다"}

{/* 기본값 및 NULL 허용 */}
updateColumn({ defaultValue: e.target.value })} placeholder="기본값 (선택사항)" disabled={loading} />
updateColumn({ nullable: !checked })} disabled={loading} />
{/* 설명 */}