diff options
Diffstat (limited to 'components/permissions/permission-group-manager.tsx')
| -rw-r--r-- | components/permissions/permission-group-manager.tsx | 294 |
1 files changed, 211 insertions, 83 deletions
diff --git a/components/permissions/permission-group-manager.tsx b/components/permissions/permission-group-manager.tsx index 11aac6cf..ff7bef7f 100644 --- a/components/permissions/permission-group-manager.tsx +++ b/components/permissions/permission-group-manager.tsx @@ -3,6 +3,9 @@ "use client"; import { useState, useEffect } from "react"; +import { useForm, Controller } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import * as z from "zod"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -20,6 +23,16 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { Select, SelectContent, SelectItem, @@ -47,6 +60,15 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; import { Shield, Plus, @@ -99,6 +121,19 @@ interface Permission { scope: string; } +// 폼 스키마 정의 +const permissionGroupFormSchema = z.object({ + groupKey: z.string() + .min(1, "그룹 키는 필수입니다.") + .regex(/^[a-z0-9_]+$/, "소문자, 숫자, 언더스코어만 사용 가능합니다."), + name: z.string().min(1, "그룹명은 필수입니다."), + description: z.string().optional(), + domain: z.string().optional(), + isActive: z.boolean().default(true), +}); + +type PermissionGroupFormValues = z.infer<typeof permissionGroupFormSchema>; + export function PermissionGroupManager() { const [groups, setGroups] = useState<PermissionGroup[]>([]); const [selectedGroup, setSelectedGroup] = useState<PermissionGroup | null>(null); @@ -109,6 +144,7 @@ export function PermissionGroupManager() { const [createDialogOpen, setCreateDialogOpen] = useState(false); const [editingGroup, setEditingGroup] = useState<PermissionGroup | null>(null); const [permissionDialogOpen, setPermissionDialogOpen] = useState(false); + const [deletingGroupId, setDeletingGroupId] = useState<number | null>(null); useEffect(() => { loadGroups(); @@ -143,19 +179,23 @@ export function PermissionGroupManager() { }; const handleDelete = async (id: number) => { - if (!confirm("이 권한 그룹을 삭제하시겠습니까? 관련된 모든 할당이 제거됩니다.")) { - return; - } + setDeletingGroupId(id); + }; + + const confirmDelete = async () => { + if (!deletingGroupId) return; try { - await deletePermissionGroup(id); + await deletePermissionGroup(deletingGroupId); toast.success("권한 그룹이 삭제되었습니다."); - if (selectedGroup?.id === id) { + if (selectedGroup?.id === deletingGroupId) { setSelectedGroup(null); } loadGroups(); } catch (error) { toast.error("권한 그룹 삭제에 실패했습니다."); + } finally { + setDeletingGroupId(null); } }; @@ -268,6 +308,24 @@ export function PermissionGroupManager() { }} /> )} + + {/* 삭제 확인 다이얼로그 */} + <AlertDialog open={!!deletingGroupId} onOpenChange={(open) => !open && setDeletingGroupId(null)}> + <AlertDialogContent> + <AlertDialogHeader> + <AlertDialogTitle>권한 그룹 삭제</AlertDialogTitle> + <AlertDialogDescription> + 이 권한 그룹을 삭제하시겠습니까? 관련된 모든 할당이 제거되며, 이 작업은 되돌릴 수 없습니다. + </AlertDialogDescription> + </AlertDialogHeader> + <AlertDialogFooter> + <AlertDialogCancel onClick={() => setDeletingGroupId(null)}>취소</AlertDialogCancel> + <AlertDialogAction onClick={confirmDelete} className="bg-destructive text-destructive-foreground hover:bg-destructive/90"> + 삭제 + </AlertDialogAction> + </AlertDialogFooter> + </AlertDialogContent> + </AlertDialog> </div> ); } @@ -290,6 +348,9 @@ function GroupCard({ onDelete: () => void; onManagePermissions: () => void; }) { + + console.log(group,"group") + return ( <Card className={cn( @@ -456,7 +517,7 @@ function GroupDetailCard({ ); } -// 그룹 생성/수정 폼 다이얼로그 +// 그룹 생성/수정 폼 다이얼로그 - react-hook-form 적용 function GroupFormDialog({ open, onOpenChange, @@ -468,48 +529,55 @@ function GroupFormDialog({ group?: PermissionGroup | null; onSuccess: () => void; }) { - const [formData, setFormData] = useState({ - groupKey: "", - name: "", - description: "", - domain: "", - isActive: true, - }); const [saving, setSaving] = useState(false); + + const form = useForm<PermissionGroupFormValues>({ + resolver: zodResolver(permissionGroupFormSchema), + defaultValues: { + groupKey: "", + name: "", + description: "", + domain: undefined, + isActive: true, + }, + }); + + console.log(form.getValues()) useEffect(() => { if (group) { - setFormData({ + form.reset({ groupKey: group.groupKey, name: group.name, description: group.description || "", - domain: group.domain || "", + domain: group.domain || undefined, isActive: group.isActive, }); } else { - setFormData({ + form.reset({ groupKey: "", name: "", description: "", - domain: "", + domain: undefined, isActive: true, }); } - }, [group]); - - const handleSubmit = async () => { - if (!formData.groupKey || !formData.name) { - toast.error("필수 항목을 입력해주세요."); - return; - } + }, [group, form]); + const onSubmit = async (values: PermissionGroupFormValues) => { setSaving(true); try { + // domain이 undefined인 경우 빈 문자열로 변환 + const submitData = { + ...values, + domain: values.domain || "", + }; + if (group) { - await updatePermissionGroup(group.id, formData); + await updatePermissionGroup(group.id, submitData); toast.success("권한 그룹이 수정되었습니다."); } else { - await createPermissionGroup(formData); + await createPermissionGroup(submitData); toast.success("권한 그룹이 생성되었습니다."); } onSuccess(); @@ -530,72 +598,131 @@ function GroupFormDialog({ </DialogDescription> </DialogHeader> - <div className="grid gap-4 py-4"> - <div> - <Label>그룹 키*</Label> - <Input - value={formData.groupKey} - onChange={(e) => setFormData({ ...formData, groupKey: e.target.value })} - placeholder="예: rfq_manager" + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> + <FormField + control={form.control} + name="groupKey" + render={({ field }) => ( + <FormItem> + <FormLabel>그룹 키 *</FormLabel> + <FormControl> + <Input + placeholder="예: rfq_manager" + {...field} + /> + </FormControl> + <FormDescription> + 소문자, 숫자, 언더스코어만 사용 가능합니다. + </FormDescription> + <FormMessage /> + </FormItem> + )} /> - </div> - <div> - <Label>그룹명*</Label> - <Input - value={formData.name} - onChange={(e) => setFormData({ ...formData, name: e.target.value })} - placeholder="예: RFQ 관리자 권한" + <FormField + control={form.control} + name="name" + render={({ field }) => ( + <FormItem> + <FormLabel>그룹명 *</FormLabel> + <FormControl> + <Input + placeholder="예: RFQ 관리자 권한" + {...field} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} /> - </div> - <div> - <Label>설명</Label> - <Textarea - value={formData.description} - onChange={(e) => setFormData({ ...formData, description: e.target.value })} - placeholder="그룹에 대한 설명" + <FormField + control={form.control} + name="description" + render={({ field }) => ( + <FormItem> + <FormLabel>설명</FormLabel> + <FormControl> + <Textarea + placeholder="그룹에 대한 설명" + {...field} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} /> - </div> - <div> - <Label>도메인</Label> - <Select - value={formData.domain} - onValueChange={(v) => setFormData({ ...formData, domain: v })} - > - <SelectTrigger> - <SelectValue placeholder="도메인 선택" /> - </SelectTrigger> - <SelectContent> - <SelectItem value="">전체</SelectItem> - <SelectItem value="evcp">EVCP</SelectItem> - <SelectItem value="partners">Partners</SelectItem> - <SelectItem value="procurement">Procurement</SelectItem> - <SelectItem value="sales">Sales</SelectItem> - <SelectItem value="engineering">Engineering</SelectItem> - </SelectContent> - </Select> - </div> + <FormField + control={form.control} + name="domain" + render={({ field }) => ( + <FormItem> + <FormLabel>도메인</FormLabel> + <Select + onValueChange={field.onChange} + value={field.value || "none"} + > + <FormControl> + <SelectTrigger> + <SelectValue placeholder="도메인 선택" /> + </SelectTrigger> + </FormControl> + <SelectContent> + <SelectItem value="evcp">eVCP</SelectItem> + <SelectItem value="partners">Partners</SelectItem> + <SelectItem value="procurement">Procurement</SelectItem> + <SelectItem value="sales">Sales</SelectItem> + <SelectItem value="engineering">Engineering</SelectItem> + </SelectContent> + </Select> + <FormDescription> + 권한 그룹이 속한 도메인을 선택하세요. + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> - <div className="flex items-center gap-2"> - <Checkbox - id="isActive" - checked={formData.isActive} - onCheckedChange={(v) => setFormData({ ...formData, isActive: !!v })} + <FormField + control={form.control} + name="isActive" + render={({ field }) => ( + <FormItem className="flex flex-row items-start space-x-3 space-y-0"> + <FormControl> + <Checkbox + checked={field.value} + onCheckedChange={field.onChange} + /> + </FormControl> + <div className="space-y-1 leading-none"> + <FormLabel> + 활성 상태 + </FormLabel> + <FormDescription> + 비활성화 시 이 그룹의 권한이 적용되지 않습니다. + </FormDescription> + </div> + </FormItem> + )} /> - <Label htmlFor="isActive">활성 상태</Label> - </div> - </div> - <DialogFooter> - <Button variant="outline" onClick={() => onOpenChange(false)}> - 취소 - </Button> - <Button onClick={handleSubmit} disabled={saving}> - {saving ? "저장 중..." : group ? "수정" : "생성"} - </Button> - </DialogFooter> + <DialogFooter> + <Button + type="button" + variant="outline" + onClick={() => onOpenChange(false)} + > + 취소 + </Button> + <Button type="submit" disabled={saving}> + {saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} + {saving ? "저장 중..." : group ? "수정" : "생성"} + </Button> + </DialogFooter> + </form> + </Form> </DialogContent> </Dialog> ); @@ -790,6 +917,7 @@ function GroupPermissionsDialog({ 취소 </Button> <Button onClick={handleSave} disabled={saving}> + {saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} {saving ? "저장 중..." : "저장"} </Button> </DialogFooter> |
