diff --git a/package.json b/package.json index dea5f4ed4b04af94d6fcdc2827e7e3dfe5df8efc..7ba36744b30ea08f8e49c89db848537eb5f96dc7 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@hookform/resolvers": "^4.1.2", + "@radix-ui/react-alert-dialog": "^1.1.6", "@radix-ui/react-avatar": "^1.1.3", "@radix-ui/react-collapsible": "^1.1.3", "@radix-ui/react-dialog": "^1.1.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dea70bbcf9bf90645183779deabfdd35f4537217..ffbe253a5c55a9e03f4c469af09235ac0babbe21 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@hookform/resolvers': specifier: ^4.1.2 version: 4.1.2(react-hook-form@7.54.2(react@19.0.0)) + '@radix-ui/react-alert-dialog': + specifier: ^1.1.6 + version: 1.1.6(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-avatar': specifier: ^1.1.3 version: 1.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -365,6 +368,19 @@ packages: '@radix-ui/primitive@1.1.1': resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==} + '@radix-ui/react-alert-dialog@1.1.6': + resolution: {integrity: sha512-p4XnPqgej8sZAAReCAKgz1REYZEBLR8hU9Pg27wFnCWIMc8g1ccCs0FjBcy05V15VTu8pAePw/VDYeOm/uZ6yQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-arrow@1.1.2': resolution: {integrity: sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==} peerDependencies: @@ -1925,6 +1941,20 @@ snapshots: '@radix-ui/primitive@1.1.1': {} + '@radix-ui/react-alert-dialog@1.1.6(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-context': 1.1.1(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-dialog': 1.1.6(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-slot': 1.1.2(@types/react@19.0.10)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.10 + '@types/react-dom': 19.0.4(@types/react@19.0.10) + '@radix-ui/react-arrow@1.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) diff --git a/src/components/ui/alert-dialog.tsx b/src/components/ui/alert-dialog.tsx new file mode 100644 index 0000000000000000000000000000000000000000..935eecf3f1453d5e4674b79c722cc49eaa1c3b2c --- /dev/null +++ b/src/components/ui/alert-dialog.tsx @@ -0,0 +1,155 @@ +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +function AlertDialog({ + ...props +}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) { + return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} /> +} + +function AlertDialogTrigger({ + ...props +}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) { + return ( + <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} /> + ) +} + +function AlertDialogPortal({ + ...props +}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) { + return ( + <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} /> + ) +} + +function AlertDialogOverlay({ + className, + ...props +}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) { + return ( + <AlertDialogPrimitive.Overlay + data-slot="alert-dialog-overlay" + className={cn( + "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50", + className + )} + {...props} + /> + ) +} + +function AlertDialogContent({ + className, + ...props +}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) { + return ( + <AlertDialogPortal> + <AlertDialogOverlay /> + <AlertDialogPrimitive.Content + data-slot="alert-dialog-content" + className={cn( + "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg", + className + )} + {...props} + /> + </AlertDialogPortal> + ) +} + +function AlertDialogHeader({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( + <div + data-slot="alert-dialog-header" + className={cn("flex flex-col gap-2 text-center sm:text-left", className)} + {...props} + /> + ) +} + +function AlertDialogFooter({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( + <div + data-slot="alert-dialog-footer" + className={cn( + "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", + className + )} + {...props} + /> + ) +} + +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) { + return ( + <AlertDialogPrimitive.Title + data-slot="alert-dialog-title" + className={cn("text-lg font-semibold", className)} + {...props} + /> + ) +} + +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) { + return ( + <AlertDialogPrimitive.Description + data-slot="alert-dialog-description" + className={cn("text-muted-foreground text-sm", className)} + {...props} + /> + ) +} + +function AlertDialogAction({ + className, + ...props +}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) { + return ( + <AlertDialogPrimitive.Action + className={cn(buttonVariants(), className)} + {...props} + /> + ) +} + +function AlertDialogCancel({ + className, + ...props +}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) { + return ( + <AlertDialogPrimitive.Cancel + className={cn(buttonVariants({ variant: "outline" }), className)} + {...props} + /> + ) +} + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/src/pages/forwarding/List.tsx b/src/pages/forwarding/List.tsx index c9392c293bbdc9ea9e1bfc98783935430be37bea..5855dfcb3cd4fa7c2da96c3adcc8fd2133f4a4fc 100644 --- a/src/pages/forwarding/List.tsx +++ b/src/pages/forwarding/List.tsx @@ -10,10 +10,21 @@ import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/h import { Input } from '@/components/ui/input'; import { Forwarding } from '@/types/forwarding'; import { Skeleton } from '@/components/ui/skeleton'; +import { + AlertDialog, + AlertDialogContent, + AlertDialogAction, + AlertDialogCancel, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogFooter, +} from '@/components/ui/alert-dialog'; export default function ForwardingList() { const { selectedProject } = useAuthStore(); const [forwardings, setForwardings] = useState<Forwarding[] | null>(null); + const [selectedForwarding, setSelectedForwarding] = useState<Forwarding | null>(null); useEffect(() => { fetch(`/api/forwardings?projectId=${selectedProject?.id}`) @@ -30,6 +41,24 @@ export default function ForwardingList() { }); }, [selectedProject]); + const handleDelete = () => { + if (selectedForwarding === null) throw Error('selectedForwarding is null'); + + fetch(`/api/forwarding?forwardingId=${selectedForwarding.id}`, { + method: 'DELETE', + }).then((response) => { + if (!response.ok) { + console.error(response); + toast.error('포트포워딩 설정 삭제에 실패했습니다'); + } else { + toast.warning('포트포워딩 설정이 삭제되었습니다'); + setForwardings((prev) => prev!.filter((forwarding) => forwarding.id !== selectedForwarding.id)); + } + }); + + setSelectedForwarding(null); + }; + return ( <div className="flex flex-1 flex-col gap-4 p-6"> <div className="flex justify-between mb-2"> @@ -120,10 +149,12 @@ export default function ForwardingList() { <Pencil /> </Link> </Button> - <Button variant="secondary" className="size-8"> - <Link to={`./delete/${forwarding.id}`}> - <Trash /> - </Link> + <Button + variant="secondary" + className="size-8" + onClick={() => setSelectedForwarding(forwarding)} + > + <Trash /> </Button> </div> </TableCell> @@ -134,6 +165,21 @@ export default function ForwardingList() { </Table> </CardContent> </Card> + + <AlertDialog open={selectedForwarding !== null} onOpenChange={(open) => open || setSelectedForwarding(null)}> + <AlertDialogContent> + <AlertDialogHeader> + <AlertDialogTitle>포트포워딩 설정 삭제</AlertDialogTitle> + <AlertDialogDescription> + 정말 '{selectedForwarding?.name}' 포트포워딩 설정을 삭제하시겠습니까? + </AlertDialogDescription> + </AlertDialogHeader> + <AlertDialogFooter> + <AlertDialogCancel>취소</AlertDialogCancel> + <AlertDialogAction onClick={handleDelete}>삭제</AlertDialogAction> + </AlertDialogFooter> + </AlertDialogContent> + </AlertDialog> </div> ); }