summaryrefslogtreecommitdiff
path: root/components/permissions/permission-group-manager.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'components/permissions/permission-group-manager.tsx')
-rw-r--r--components/permissions/permission-group-manager.tsx294
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>