From 996574c8c409c540b5589356003011dea2fb0282 Mon Sep 17 00:00:00 2001 From: 6-keem <6ukeem@gmail.com> Date: Wed, 26 Feb 2025 14:00:02 +0900 Subject: [PATCH] feat: Add pending dialog --- package.json | 2 +- src/components/ui/dialog.tsx | 120 +++++++++++++++++++++ src/content/App.tsx | 10 ++ src/content/components/pending-dialog.tsx | 124 ++++++++++++++++++++++ src/styles/index.css | 11 ++ src/styles/shadow.css | 9 ++ 6 files changed, 275 insertions(+), 1 deletion(-) create mode 100644 src/components/ui/dialog.tsx create mode 100644 src/content/components/pending-dialog.tsx diff --git a/package.json b/package.json index 51e10d4..d3ab5f1 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "@heroui/theme": "^2.4.6", "@radix-ui/react-avatar": "^1.1.2", "@radix-ui/react-checkbox": "^1.1.3", - "@radix-ui/react-dialog": "^1.1.5", + "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-label": "^2.1.1", "@radix-ui/react-popover": "^1.1.4", "@radix-ui/react-progress": "^1.1.1", diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx new file mode 100644 index 0000000..9dbeaa0 --- /dev/null +++ b/src/components/ui/dialog.tsx @@ -0,0 +1,120 @@ +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogTrigger, + DialogClose, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/src/content/App.tsx b/src/content/App.tsx index 4994ae7..5fb48a3 100644 --- a/src/content/App.tsx +++ b/src/content/App.tsx @@ -17,6 +17,7 @@ import FilterBadge from './components/FilterBadge'; import FilterPanel from './components/FilterPanel'; import { useCourseData } from '@/hooks/useCourseData'; import { filterVods, filterAssigns, filterQuizes } from '@/lib/filterData'; +import PendingDialogWithBeforeUnload from './components/pending-dialog'; // 리팩토링: 필터 옵션 추출 const attendanceOptions = ['출석', '결석']; // string[] @@ -149,6 +150,14 @@ export default function App() { }; return ( + <> + { + // 필요한 경우 강제 종료 처리 + console.log("사용자가 다이얼로그를 닫았습니다") + }} + /> {isOpen ? ( @@ -318,5 +327,6 @@ export default function App() { + ); } diff --git a/src/content/components/pending-dialog.tsx b/src/content/components/pending-dialog.tsx new file mode 100644 index 0000000..9782fcb --- /dev/null +++ b/src/content/components/pending-dialog.tsx @@ -0,0 +1,124 @@ +import { useState, useEffect } from 'react'; +import ReactDOM from 'react-dom'; +import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Loader2 } from 'lucide-react'; +import styles from '@/styles/shadow.css?inline'; +import { createShadowRoot } from '@/lib/createShadowRoot'; + +interface PendingDialogProps { + isPending: boolean; + onClose?: () => void; +} + +export default function PendingDialog({ isPending, onClose }: PendingDialogProps) { + const [open, setOpen] = useState(false); + const [cancelEnabled, setCancelEnabled] = useState(false); + const [modalContainer, setModalContainer] = useState(null); + const [hostElement, setHostElement] = useState(null); + + useEffect(() => { + let host = document.getElementById('shadow-modal-host') as HTMLElement | null; + if (!host) { + host = document.createElement('div'); + host.id = 'shadow-modal-host'; + host.style.zIndex = '9999'; + host.style.backgroundColor = 'rgba(0, 0, 0, 0.7)'; + document.body.prepend(host); + } + setHostElement(host); + const newShadowRoot = createShadowRoot(host, [ + styles, + ` + :host { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; + } + `, + ]); + setModalContainer(newShadowRoot); + }, []); + + // open 상태에 따라 host의 display 업데이트 + useEffect(() => { + if (hostElement) { + hostElement.style.display = open ? 'flex' : 'none'; + } + }, [open, hostElement]); + + // isPending 상태에 따라 모달 열림/닫힘 관리 + useEffect(() => { + if (isPending) { + setOpen(true); + setCancelEnabled(false); + } else { + setOpen(false); + setCancelEnabled(false); + } + }, [isPending]); + + // 모달이 열리면 스크롤을 막고, 닫히면 복구 + useEffect(() => { + if (open) { + document.body.style.overflow = 'hidden'; + } else { + document.body.style.overflow = ''; + } + }, [open]); + + // 모달이 열릴 때 15초 후에 취소 버튼 활성화 + useEffect(() => { + let timer: NodeJS.Timeout; + if (open) { + timer = setTimeout(() => { + setCancelEnabled(true); + }, 15000); + } + return () => { + clearTimeout(timer); + }; + }, [open]); + + const handleClose = () => { + // 취소 버튼이 활성화된 경우에만 닫힘 동작 실행 + if (cancelEnabled) { + setOpen(false); + if (onClose) { + onClose(); + } + } + }; + + const modalContent = ( + + + 요청 진행 중 +

요청이 진행중입니다. 잠시만 기다려주세요.

+
+ +
+ +

완료되면 자동으로 창이 닫힙니다.

+
+ +
+
+ ); + + if (!modalContainer) return null; + return ReactDOM.createPortal(modalContent, modalContainer); +} diff --git a/src/styles/index.css b/src/styles/index.css index 636fa3a..1963fd0 100755 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -126,3 +126,14 @@ h1 { @apply bg-background text-foreground; } } + + + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/src/styles/shadow.css b/src/styles/shadow.css index f957eb4..2e0a37d 100644 --- a/src/styles/shadow.css +++ b/src/styles/shadow.css @@ -167,3 +167,12 @@ overscroll-behavior: none; } } + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +}