- Replaced existing toast error messages with the new `showErrorToast` utility across multiple components, improving consistency in error reporting. - Updated error messages to provide more specific guidance for users, enhancing the overall user experience during error scenarios. - Ensured that all relevant error handling in batch management, external call configurations, cascading management, and screen management components now utilizes the new utility for better maintainability.
688 lines
26 KiB
TypeScript
688 lines
26 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect, useCallback } from "react";
|
|
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 { Switch } from "@/components/ui/switch";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
} from "@/components/ui/alert-dialog";
|
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
import { Separator } from "@/components/ui/separator";
|
|
import {
|
|
Check,
|
|
ChevronsUpDown,
|
|
Plus,
|
|
Pencil,
|
|
Trash2,
|
|
Search,
|
|
RefreshCw,
|
|
ArrowRight,
|
|
X,
|
|
GripVertical,
|
|
} from "lucide-react";
|
|
import { toast } from "sonner";
|
|
import { showErrorToast } from "@/lib/utils/toastUtils";
|
|
import { cn } from "@/lib/utils";
|
|
import { cascadingAutoFillApi, AutoFillGroup, AutoFillMapping } from "@/lib/api/cascadingAutoFill";
|
|
import { tableManagementApi } from "@/lib/api/tableManagement";
|
|
|
|
interface TableColumn {
|
|
columnName: string;
|
|
columnLabel?: string;
|
|
dataType?: string;
|
|
}
|
|
|
|
export default function AutoFillTab() {
|
|
// 목록 상태
|
|
const [groups, setGroups] = useState<AutoFillGroup[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [searchText, setSearchText] = useState("");
|
|
|
|
// 모달 상태
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
|
const [editingGroup, setEditingGroup] = useState<AutoFillGroup | null>(null);
|
|
const [deletingGroupCode, setDeletingGroupCode] = useState<string | null>(null);
|
|
|
|
// 테이블/컬럼 목록
|
|
const [tableList, setTableList] = useState<Array<{ tableName: string; displayName?: string }>>([]);
|
|
const [masterColumns, setMasterColumns] = useState<TableColumn[]>([]);
|
|
|
|
// 폼 데이터
|
|
const [formData, setFormData] = useState({
|
|
groupName: "",
|
|
description: "",
|
|
masterTable: "",
|
|
masterValueColumn: "",
|
|
masterLabelColumn: "",
|
|
isActive: "Y",
|
|
});
|
|
|
|
// 매핑 데이터
|
|
const [mappings, setMappings] = useState<AutoFillMapping[]>([]);
|
|
|
|
// 테이블 Combobox 상태
|
|
const [tableComboOpen, setTableComboOpen] = useState(false);
|
|
|
|
// 목록 로드
|
|
const loadGroups = useCallback(async () => {
|
|
setLoading(true);
|
|
try {
|
|
const response = await cascadingAutoFillApi.getGroups();
|
|
if (response.success && response.data) {
|
|
setGroups(response.data);
|
|
}
|
|
} catch (error) {
|
|
console.error("그룹 목록 로드 실패:", error);
|
|
showErrorToast("그룹 목록을 불러오는 데 실패했습니다", error, { guidance: "네트워크 연결을 확인해 주세요." });
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
// 테이블 목록 로드
|
|
const loadTableList = useCallback(async () => {
|
|
try {
|
|
const response = await tableManagementApi.getTableList();
|
|
if (response.success && response.data) {
|
|
setTableList(response.data);
|
|
}
|
|
} catch (error) {
|
|
console.error("테이블 목록 로드 실패:", error);
|
|
}
|
|
}, []);
|
|
|
|
// 테이블 컬럼 로드
|
|
const loadColumns = useCallback(async (tableName: string) => {
|
|
if (!tableName) {
|
|
setMasterColumns([]);
|
|
return;
|
|
}
|
|
try {
|
|
const response = await tableManagementApi.getColumnList(tableName);
|
|
if (response.success && response.data?.columns) {
|
|
setMasterColumns(
|
|
response.data.columns.map((col: any) => ({
|
|
columnName: col.columnName || col.column_name,
|
|
columnLabel: col.columnLabel || col.column_label || col.columnName,
|
|
dataType: col.dataType || col.data_type,
|
|
})),
|
|
);
|
|
}
|
|
} catch (error) {
|
|
console.error("컬럼 목록 로드 실패:", error);
|
|
setMasterColumns([]);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
loadGroups();
|
|
loadTableList();
|
|
}, [loadGroups, loadTableList]);
|
|
|
|
// 테이블 변경 시 컬럼 로드
|
|
useEffect(() => {
|
|
if (formData.masterTable) {
|
|
loadColumns(formData.masterTable);
|
|
}
|
|
}, [formData.masterTable, loadColumns]);
|
|
|
|
// 필터된 목록
|
|
const filteredGroups = groups.filter(
|
|
(g) =>
|
|
g.groupCode.toLowerCase().includes(searchText.toLowerCase()) ||
|
|
g.groupName.toLowerCase().includes(searchText.toLowerCase()) ||
|
|
g.masterTable?.toLowerCase().includes(searchText.toLowerCase()),
|
|
);
|
|
|
|
// 모달 열기 (생성)
|
|
const handleOpenCreate = () => {
|
|
setEditingGroup(null);
|
|
setFormData({
|
|
groupName: "",
|
|
description: "",
|
|
masterTable: "",
|
|
masterValueColumn: "",
|
|
masterLabelColumn: "",
|
|
isActive: "Y",
|
|
});
|
|
setMappings([]);
|
|
setMasterColumns([]);
|
|
setIsModalOpen(true);
|
|
};
|
|
|
|
// 모달 열기 (수정)
|
|
const handleOpenEdit = async (group: AutoFillGroup) => {
|
|
setEditingGroup(group);
|
|
|
|
// 상세 정보 로드
|
|
const detailResponse = await cascadingAutoFillApi.getGroupDetail(group.groupCode);
|
|
if (detailResponse.success && detailResponse.data) {
|
|
const detail = detailResponse.data;
|
|
|
|
// 컬럼 먼저 로드
|
|
if (detail.masterTable) {
|
|
await loadColumns(detail.masterTable);
|
|
}
|
|
|
|
setFormData({
|
|
groupCode: detail.groupCode,
|
|
groupName: detail.groupName,
|
|
description: detail.description || "",
|
|
masterTable: detail.masterTable,
|
|
masterValueColumn: detail.masterValueColumn,
|
|
masterLabelColumn: detail.masterLabelColumn || "",
|
|
isActive: detail.isActive || "Y",
|
|
});
|
|
|
|
// 매핑 데이터 변환 (snake_case → camelCase)
|
|
const convertedMappings = (detail.mappings || []).map((m: any) => ({
|
|
sourceColumn: m.source_column || m.sourceColumn,
|
|
targetField: m.target_field || m.targetField,
|
|
targetLabel: m.target_label || m.targetLabel || "",
|
|
isEditable: m.is_editable || m.isEditable || "Y",
|
|
isRequired: m.is_required || m.isRequired || "N",
|
|
defaultValue: m.default_value || m.defaultValue || "",
|
|
sortOrder: m.sort_order || m.sortOrder || 0,
|
|
}));
|
|
setMappings(convertedMappings);
|
|
}
|
|
|
|
setIsModalOpen(true);
|
|
};
|
|
|
|
// 삭제 확인
|
|
const handleDeleteConfirm = (groupCode: string) => {
|
|
setDeletingGroupCode(groupCode);
|
|
setIsDeleteDialogOpen(true);
|
|
};
|
|
|
|
// 삭제 실행
|
|
const handleDelete = async () => {
|
|
if (!deletingGroupCode) return;
|
|
|
|
try {
|
|
const response = await cascadingAutoFillApi.deleteGroup(deletingGroupCode);
|
|
if (response.success) {
|
|
toast.success("자동 입력 그룹이 삭제되었습니다.");
|
|
loadGroups();
|
|
} else {
|
|
toast.error(response.error || "삭제에 실패했습니다.");
|
|
}
|
|
} catch (error) {
|
|
toast.error("삭제 중 오류가 발생했습니다.");
|
|
} finally {
|
|
setIsDeleteDialogOpen(false);
|
|
setDeletingGroupCode(null);
|
|
}
|
|
};
|
|
|
|
// 저장
|
|
const handleSave = async () => {
|
|
// 유효성 검사
|
|
if (!formData.groupName || !formData.masterTable || !formData.masterValueColumn) {
|
|
toast.error("필수 항목을 모두 입력해주세요.");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const saveData = {
|
|
...formData,
|
|
mappings,
|
|
};
|
|
|
|
let response;
|
|
if (editingGroup) {
|
|
response = await cascadingAutoFillApi.updateGroup(editingGroup.groupCode!, saveData);
|
|
} else {
|
|
response = await cascadingAutoFillApi.createGroup(saveData);
|
|
}
|
|
|
|
if (response.success) {
|
|
toast.success(editingGroup ? "수정되었습니다." : "생성되었습니다.");
|
|
setIsModalOpen(false);
|
|
loadGroups();
|
|
} else {
|
|
toast.error(response.error || "저장에 실패했습니다.");
|
|
}
|
|
} catch (error) {
|
|
showErrorToast("자동입력 설정 저장에 실패했습니다", error, { guidance: "입력 데이터를 확인하고 다시 시도해 주세요." });
|
|
}
|
|
};
|
|
|
|
// 매핑 추가
|
|
const handleAddMapping = () => {
|
|
setMappings([
|
|
...mappings,
|
|
{
|
|
sourceColumn: "",
|
|
targetField: "",
|
|
targetLabel: "",
|
|
isEditable: "Y",
|
|
isRequired: "N",
|
|
defaultValue: "",
|
|
sortOrder: mappings.length + 1,
|
|
},
|
|
]);
|
|
};
|
|
|
|
// 매핑 삭제
|
|
const handleRemoveMapping = (index: number) => {
|
|
setMappings(mappings.filter((_, i) => i !== index));
|
|
};
|
|
|
|
// 매핑 수정
|
|
const handleMappingChange = (index: number, field: keyof AutoFillMapping, value: any) => {
|
|
const updated = [...mappings];
|
|
updated[index] = { ...updated[index], [field]: value };
|
|
setMappings(updated);
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* 검색 및 액션 */}
|
|
<Card>
|
|
<CardContent className="pt-6">
|
|
<div className="flex items-center gap-4">
|
|
<div className="relative flex-1">
|
|
<Search className="text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2" />
|
|
<Input
|
|
placeholder="그룹 코드, 이름, 테이블명으로 검색..."
|
|
value={searchText}
|
|
onChange={(e) => setSearchText(e.target.value)}
|
|
className="pl-10"
|
|
/>
|
|
</div>
|
|
<Button variant="outline" onClick={loadGroups}>
|
|
<RefreshCw className="mr-2 h-4 w-4" />
|
|
새로고침
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* 목록 */}
|
|
<Card>
|
|
<CardHeader>
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<CardTitle>자동 입력 그룹</CardTitle>
|
|
<CardDescription>
|
|
마스터 선택 시 여러 필드를 자동으로 입력합니다. (총 {filteredGroups.length}개)
|
|
</CardDescription>
|
|
</div>
|
|
<Button onClick={handleOpenCreate}>
|
|
<Plus className="mr-2 h-4 w-4" />새 그룹 추가
|
|
</Button>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{loading ? (
|
|
<div className="flex items-center justify-center py-8">
|
|
<RefreshCw className="h-6 w-6 animate-spin" />
|
|
<span className="ml-2">로딩 중...</span>
|
|
</div>
|
|
) : filteredGroups.length === 0 ? (
|
|
<div className="text-muted-foreground py-8 text-center">
|
|
{searchText ? "검색 결과가 없습니다." : "등록된 자동 입력 그룹이 없습니다."}
|
|
</div>
|
|
) : (
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>그룹 코드</TableHead>
|
|
<TableHead>그룹명</TableHead>
|
|
<TableHead>마스터 테이블</TableHead>
|
|
<TableHead>매핑 수</TableHead>
|
|
<TableHead>상태</TableHead>
|
|
<TableHead className="text-right">작업</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{filteredGroups.map((group) => (
|
|
<TableRow key={group.groupCode}>
|
|
<TableCell className="font-mono text-sm">{group.groupCode}</TableCell>
|
|
<TableCell className="font-medium">{group.groupName}</TableCell>
|
|
<TableCell className="text-muted-foreground">{group.masterTable}</TableCell>
|
|
<TableCell>
|
|
<Badge variant="secondary">{group.mappingCount || 0}개</Badge>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Badge variant={group.isActive === "Y" ? "default" : "outline"}>
|
|
{group.isActive === "Y" ? "활성" : "비활성"}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell className="text-right">
|
|
<Button variant="ghost" size="icon" onClick={() => handleOpenEdit(group)}>
|
|
<Pencil className="h-4 w-4" />
|
|
</Button>
|
|
<Button variant="ghost" size="icon" onClick={() => handleDeleteConfirm(group.groupCode)}>
|
|
<Trash2 className="h-4 w-4 text-red-500" />
|
|
</Button>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* 생성/수정 모달 */}
|
|
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
|
|
<DialogContent className="max-h-[90vh] max-w-4xl overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle>{editingGroup ? "자동 입력 그룹 수정" : "자동 입력 그룹 생성"}</DialogTitle>
|
|
<DialogDescription>마스터 데이터 선택 시 자동으로 입력할 필드들을 설정합니다.</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-6">
|
|
{/* 기본 정보 */}
|
|
<div className="space-y-4">
|
|
<h3 className="text-sm font-semibold">기본 정보</h3>
|
|
|
|
<div className="space-y-2">
|
|
<Label>그룹명 *</Label>
|
|
<Input
|
|
value={formData.groupName}
|
|
onChange={(e) => setFormData({ ...formData, groupName: e.target.value })}
|
|
placeholder="예: 고객사 정보 자동입력"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label>설명</Label>
|
|
<Textarea
|
|
value={formData.description}
|
|
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
|
placeholder="이 자동 입력 그룹에 대한 설명"
|
|
rows={2}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-2">
|
|
<Switch
|
|
checked={formData.isActive === "Y"}
|
|
onCheckedChange={(checked) => setFormData({ ...formData, isActive: checked ? "Y" : "N" })}
|
|
/>
|
|
<Label>활성화</Label>
|
|
</div>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
{/* 마스터 테이블 설정 */}
|
|
<div className="space-y-4">
|
|
<h3 className="text-sm font-semibold">마스터 테이블 설정</h3>
|
|
<p className="text-muted-foreground text-xs">
|
|
사용자가 선택할 마스터 데이터의 테이블과 컬럼을 지정합니다.
|
|
</p>
|
|
|
|
<div className="grid grid-cols-3 gap-4">
|
|
<div className="space-y-2">
|
|
<Label>마스터 테이블 *</Label>
|
|
<Popover open={tableComboOpen} onOpenChange={setTableComboOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
aria-expanded={tableComboOpen}
|
|
className="h-10 w-full justify-between text-sm"
|
|
>
|
|
{formData.masterTable
|
|
? tableList.find((t) => t.tableName === formData.masterTable)?.displayName ||
|
|
formData.masterTable
|
|
: "테이블 선택"}
|
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent
|
|
className="p-0"
|
|
style={{ width: "var(--radix-popover-trigger-width)" }}
|
|
align="start"
|
|
>
|
|
<Command>
|
|
<CommandInput placeholder="테이블명 또는 라벨로 검색..." className="text-sm" />
|
|
<CommandList className="max-h-[300px] overflow-y-auto overscroll-contain">
|
|
<CommandEmpty className="text-sm">테이블을 찾을 수 없습니다.</CommandEmpty>
|
|
<CommandGroup>
|
|
{tableList.map((table) => (
|
|
<CommandItem
|
|
key={table.tableName}
|
|
value={`${table.tableName} ${table.displayName || ""}`}
|
|
onSelect={() => {
|
|
setFormData({
|
|
...formData,
|
|
masterTable: table.tableName,
|
|
masterValueColumn: "",
|
|
masterLabelColumn: "",
|
|
});
|
|
setTableComboOpen(false);
|
|
}}
|
|
className="text-sm"
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-4 w-4",
|
|
formData.masterTable === table.tableName ? "opacity-100" : "opacity-0",
|
|
)}
|
|
/>
|
|
<div className="flex flex-col">
|
|
<span className="font-medium">{table.displayName || table.tableName}</span>
|
|
{table.displayName && table.displayName !== table.tableName && (
|
|
<span className="text-muted-foreground text-xs">{table.tableName}</span>
|
|
)}
|
|
</div>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label>값 컬럼 *</Label>
|
|
<Select
|
|
value={formData.masterValueColumn}
|
|
onValueChange={(value) => setFormData({ ...formData, masterValueColumn: value })}
|
|
disabled={!formData.masterTable}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="값 컬럼 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{masterColumns.map((col) => (
|
|
<SelectItem key={col.columnName} value={col.columnName}>
|
|
{col.columnLabel || col.columnName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label>라벨 컬럼</Label>
|
|
<Select
|
|
value={formData.masterLabelColumn}
|
|
onValueChange={(value) => setFormData({ ...formData, masterLabelColumn: value })}
|
|
disabled={!formData.masterTable}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="라벨 컬럼 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{masterColumns.map((col) => (
|
|
<SelectItem key={col.columnName} value={col.columnName}>
|
|
{col.columnLabel || col.columnName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
{/* 필드 매핑 */}
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h3 className="text-sm font-semibold">필드 매핑</h3>
|
|
<p className="text-muted-foreground text-xs">
|
|
마스터 테이블의 컬럼을 폼의 어떤 필드에 자동 입력할지 설정합니다.
|
|
</p>
|
|
</div>
|
|
<Button variant="outline" size="sm" onClick={handleAddMapping}>
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
매핑 추가
|
|
</Button>
|
|
</div>
|
|
|
|
{mappings.length === 0 ? (
|
|
<div className="text-muted-foreground rounded-lg border border-dashed py-8 text-center text-sm">
|
|
매핑이 없습니다. "매핑 추가" 버튼을 클릭하여 추가하세요.
|
|
</div>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{mappings.map((mapping, index) => (
|
|
<div key={index} className="bg-muted/30 flex items-center gap-3 rounded-lg border p-3">
|
|
<GripVertical className="text-muted-foreground h-4 w-4 cursor-move" />
|
|
|
|
{/* 소스 컬럼 */}
|
|
<div className="w-40">
|
|
<Select
|
|
value={mapping.sourceColumn}
|
|
onValueChange={(value) => handleMappingChange(index, "sourceColumn", value)}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs">
|
|
<SelectValue placeholder="소스 컬럼" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{masterColumns.map((col) => (
|
|
<SelectItem key={col.columnName} value={col.columnName}>
|
|
{col.columnLabel || col.columnName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<ArrowRight className="text-muted-foreground h-4 w-4" />
|
|
|
|
{/* 타겟 필드 */}
|
|
<div className="flex-1">
|
|
<Input
|
|
value={mapping.targetField}
|
|
onChange={(e) => handleMappingChange(index, "targetField", e.target.value)}
|
|
placeholder="타겟 필드명 (예: contact_name)"
|
|
className="h-8 text-xs"
|
|
/>
|
|
</div>
|
|
|
|
{/* 타겟 라벨 */}
|
|
<div className="w-28">
|
|
<Input
|
|
value={mapping.targetLabel || ""}
|
|
onChange={(e) => handleMappingChange(index, "targetLabel", e.target.value)}
|
|
placeholder="라벨"
|
|
className="h-8 text-xs"
|
|
/>
|
|
</div>
|
|
|
|
{/* 옵션 */}
|
|
<div className="flex items-center gap-2">
|
|
<div className="flex items-center space-x-1">
|
|
<Checkbox
|
|
id={`editable-${index}`}
|
|
checked={mapping.isEditable === "Y"}
|
|
onCheckedChange={(checked) => handleMappingChange(index, "isEditable", checked ? "Y" : "N")}
|
|
/>
|
|
<Label htmlFor={`editable-${index}`} className="text-xs">
|
|
수정
|
|
</Label>
|
|
</div>
|
|
<div className="flex items-center space-x-1">
|
|
<Checkbox
|
|
id={`required-${index}`}
|
|
checked={mapping.isRequired === "Y"}
|
|
onCheckedChange={(checked) => handleMappingChange(index, "isRequired", checked ? "Y" : "N")}
|
|
/>
|
|
<Label htmlFor={`required-${index}`} className="text-xs">
|
|
필수
|
|
</Label>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 삭제 버튼 */}
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8"
|
|
onClick={() => handleRemoveMapping(index)}
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => setIsModalOpen(false)}>
|
|
취소
|
|
</Button>
|
|
<Button onClick={handleSave}>{editingGroup ? "수정" : "생성"}</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* 삭제 확인 다이얼로그 */}
|
|
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>자동 입력 그룹 삭제</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
이 자동 입력 그룹을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>취소</AlertDialogCancel>
|
|
<AlertDialogAction onClick={handleDelete} className="bg-red-500 hover:bg-red-600">
|
|
삭제
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</div>
|
|
);
|
|
}
|