ERP-node/frontend/components/admin/ColumnDefinitionTable.tsx

322 lines
12 KiB
TypeScript
Raw Normal View History

/**
*
*
*/
"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,
VALIDATION_RULES,
RESERVED_WORDS,
RESERVED_COLUMNS,
} from "../../types/ddl";
2025-09-23 10:40:21 +09:00
import { INPUT_TYPE_OPTIONS } from "../../types/input-types";
export function ColumnDefinitionTable({ columns, onChange, disabled = false }: ColumnDefinitionTableProps) {
const [validationErrors, setValidationErrors] = useState<Record<number, string[]>>({});
/**
*
*/
const updateColumn = (index: number, updates: Partial<CreateColumnDefinition>) => {
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<number, string[]> = {};
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("중복된 컬럼명입니다");
}
}
2025-09-23 10:40:21 +09:00
// 입력타입 검증
if (!column.inputType) {
errors.push("입력타입을 선택해주세요");
}
// 길이 검증 (길이를 지원하는 타입인 경우)
2025-09-23 10:40:21 +09:00
const inputTypeOption = INPUT_TYPE_OPTIONS.find((opt) => opt.value === column.inputType);
if (inputTypeOption?.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;
};
/**
2025-09-23 10:40:21 +09:00
*
*/
2025-09-23 10:40:21 +09:00
const handleInputTypeChange = (index: number, inputType: string) => {
const inputTypeOption = INPUT_TYPE_OPTIONS.find((opt) => opt.value === inputType);
const updates: Partial<CreateColumnDefinition> = { inputType: inputType as any };
// 길이를 지원하는 타입이고 현재 길이가 없으면 기본값 설정
2025-09-23 10:40:21 +09:00
if (inputTypeOption?.supportsLength && !columns[index].length && inputTypeOption.defaultLength) {
updates.length = inputTypeOption.defaultLength;
}
// 길이를 지원하지 않는 타입이면 길이 제거
2025-09-23 10:40:21 +09:00
if (!inputTypeOption?.supportsLength) {
updates.length = undefined;
}
updateColumn(index, updates);
};
/**
*
*/
const hasValidationErrors = Object.values(validationErrors).some((errors) => errors.length > 0);
return (
<div className="space-y-4">
{/* 검증 오류 요약 */}
{hasValidationErrors && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription> . .</AlertDescription>
</Alert>
)}
{/* 컬럼 정의 테이블 */}
<div className="overflow-hidden bg-card shadow-sm">
<Table>
<TableHeader>
<TableRow>
<TableHead className="h-12 w-[150px] text-sm font-semibold">
<span className="text-destructive">*</span>
</TableHead>
<TableHead className="h-12 w-[150px] text-sm font-semibold"></TableHead>
<TableHead className="h-12 w-[120px] text-sm font-semibold">
<span className="text-destructive">*</span>
</TableHead>
<TableHead className="h-12 w-[80px] text-sm font-semibold"></TableHead>
<TableHead className="h-12 w-[100px] text-sm font-semibold"></TableHead>
<TableHead className="h-12 w-[120px] text-sm font-semibold"></TableHead>
<TableHead className="h-12 text-sm font-semibold"></TableHead>
<TableHead className="h-12 w-[50px] text-sm font-semibold"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{columns.map((column, index) => {
2025-09-23 10:40:21 +09:00
const inputTypeOption = INPUT_TYPE_OPTIONS.find((opt) => opt.value === column.inputType);
const rowErrors = validationErrors[index] || [];
const hasRowError = rowErrors.length > 0;
return (
<TableRow key={index} className={`transition-colors hover:bg-muted/50 ${hasRowError ? "bg-destructive/10" : ""}`}>
<TableCell className="h-16">
<div className="space-y-1">
<Input
value={column.name}
onChange={(e) => updateColumn(index, { name: e.target.value })}
placeholder="column_name"
disabled={disabled}
className={`text-sm ${hasRowError ? "border-destructive" : ""}`}
/>
{rowErrors.length > 0 && (
<div className="space-y-1 text-xs text-destructive">
{rowErrors.map((error, i) => (
<div key={i}>{error}</div>
))}
</div>
)}
</div>
</TableCell>
<TableCell className="h-16 text-sm">
<Input
value={column.label || ""}
onChange={(e) => updateColumn(index, { label: e.target.value })}
placeholder="컬럼 라벨"
disabled={disabled}
className="text-sm"
/>
</TableCell>
<TableCell className="h-16 text-sm">
<Select
2025-09-23 10:40:21 +09:00
value={column.inputType}
onValueChange={(value) => handleInputTypeChange(index, value)}
disabled={disabled}
>
<SelectTrigger className="text-sm">
2025-09-23 10:40:21 +09:00
<SelectValue placeholder="입력 타입 선택" />
</SelectTrigger>
<SelectContent>
2025-09-23 10:40:21 +09:00
{INPUT_TYPE_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
<div>
<div className="font-medium">{option.label}</div>
{option.description && (
<div className="text-muted-foreground text-xs">{option.description}</div>
)}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</TableCell>
<TableCell className="h-16 text-sm">
<div className="flex items-center justify-center">
<Checkbox
checked={!column.nullable}
onCheckedChange={(checked) => updateColumn(index, { nullable: !checked })}
disabled={disabled}
/>
</div>
</TableCell>
<TableCell className="h-16 text-sm">
<Input
type="number"
value={column.length || ""}
onChange={(e) =>
updateColumn(index, {
length: e.target.value ? parseInt(e.target.value) : undefined,
})
}
2025-09-23 10:40:21 +09:00
placeholder={inputTypeOption?.defaultLength?.toString() || ""}
disabled={disabled || !inputTypeOption?.supportsLength}
min={1}
max={65535}
className="text-sm"
/>
</TableCell>
<TableCell className="h-16 text-sm">
<Input
value={column.defaultValue || ""}
onChange={(e) => updateColumn(index, { defaultValue: e.target.value })}
placeholder="기본값"
disabled={disabled}
className="text-sm"
/>
</TableCell>
<TableCell className="h-16 text-sm">
<Textarea
value={column.description || ""}
onChange={(e) => updateColumn(index, { description: e.target.value })}
placeholder="컬럼 설명"
disabled={disabled}
rows={1}
className="min-h-[36px] resize-none text-sm"
/>
</TableCell>
<TableCell className="h-16 text-sm">
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => removeColumn(index)}
disabled={disabled || columns.length === 1}
className="h-8 w-8 p-0"
>
<X className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
{/* 도움말 */}
<div className="text-muted-foreground space-y-1 text-sm">
<div> , , .</div>
<div> 추가됩니다: id (PK), created_date, updated_date, company_code</div>
<div> NOT NULL .</div>
<div> text, code, email, tel .</div>
</div>
</div>
);
}