summaryrefslogtreecommitdiff
path: root/components/information
diff options
context:
space:
mode:
Diffstat (limited to 'components/information')
-rw-r--r--components/information/information-button.tsx182
1 files changed, 177 insertions, 5 deletions
diff --git a/components/information/information-button.tsx b/components/information/information-button.tsx
index e9163093..e03fffd9 100644
--- a/components/information/information-button.tsx
+++ b/components/information/information-button.tsx
@@ -9,6 +9,7 @@ import {
DialogContent,
DialogHeader,
DialogTitle,
+ DialogDescription,
DialogTrigger,
} from "@/components/ui/dialog"
import { Info, Download, Edit, Loader2,Eye, EyeIcon } from "lucide-react"
@@ -18,6 +19,13 @@ import { UpdateInformationDialog } from "@/lib/information/table/update-informat
import { NoticeViewDialog } from "@/components/notice/notice-view-dialog"
// import { PDFTronViewerDialog } from "@/components/document-viewer/pdftron-viewer-dialog" // 주석 처리 - 브라우저 내장 뷰어 사용
import type { PageInformation, InformationAttachment } from "@/db/schema/information"
+import { isNoticeDontShowValid, setNoticeDontShow } from "@/lib/notice/storage-utils"
+import { Checkbox } from "@/components/ui/checkbox"
+import { Badge } from "@/components/ui/badge"
+import { Separator } from "@/components/ui/separator"
+import { ScrollArea } from "@/components/ui/scroll-area"
+import { AlertCircle, Calendar, Clock, User } from "lucide-react"
+import { toast } from "sonner"
type PageInformationWithUpdatedBy = PageInformation & {
updatedByName?: string | null
@@ -39,10 +47,11 @@ interface InformationButtonProps {
type NoticeWithAuthor = Notice & {
authorName: string | null
authorEmail: string | null
+ isPopup?: boolean
}
-export function InformationButton({
- pagePath,
+export function InformationButton({
+ pagePath,
className,
variant = "ghost",
size = "icon"
@@ -58,6 +67,11 @@ export function InformationButton({
const [dataLoaded, setDataLoaded] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const [retryCount, setRetryCount] = useState(0)
+
+ // 강제 모달 관련 상태
+ const [forceModalNotice, setForceModalNotice] = useState<NoticeWithAuthor | null>(null)
+ const [isForceModalOpen, setIsForceModalOpen] = useState(false)
+ const [forceModalDontShow, setForceModalDontShow] = useState(false)
// const [viewerDialogOpen, setViewerDialogOpen] = useState(false) // 주석 처리 - 브라우저 내장 뷰어 사용
// const [selectedFile, setSelectedFile] = useState<InformationAttachment | null>(null) // 주석 처리 - 브라우저 내장 뷰어 사용
@@ -85,11 +99,14 @@ export function InformationButton({
// 순차적으로 데이터 조회 (프로덕션 안정성)
const infoResult = await getPageInformationDirect(normalizedPath)
const noticesResult = await getPageNotices(normalizedPath)
-
+
setInformation(infoResult)
setNotices(noticesResult)
setDataLoaded(true)
setRetryCount(0) // 성공시 재시도 횟수 리셋
+
+ // 강제 모달을 띄워야 할 공지사항 확인
+ checkForceModalNotices(noticesResult)
// 권한 확인 - 세션이 확실히 있을 때만
if (session?.user?.id && infoResult) {
@@ -119,9 +136,9 @@ export function InformationButton({
}
}, [pagePath, session?.user?.id, dataLoaded, retryCount])
- // 세션이 준비되면 자동으로 데이터 로드
+ // 세션이 준비되면 자동으로 데이터 로드 (버튼 클릭 시)
React.useEffect(() => {
- if (isOpen && !dataLoaded && session !== undefined) {
+ if (!dataLoaded && session !== undefined) {
loadData()
}
}, [isOpen, dataLoaded, session])
@@ -136,6 +153,59 @@ export function InformationButton({
}
}, [retryCount])
+ // 강제 모달을 띄워야 할 공지사항 확인 함수
+ const checkForceModalNotices = React.useCallback((noticesList: NoticeWithAuthor[]) => {
+ // 여기서 유효기간 필터까지 처리
+ if (!noticesList || noticesList.length === 0) return
+
+ const now = new Date()
+
+ for (const notice of noticesList) {
+ // 팝업 공지사항이 아니면 건너뛰기
+ if (!notice.isPopup) continue
+
+ // 유효기간 필터링: startAt과 endAt이 모두 null이거나, 현재가 범위 내에 있어야
+ const validStart = !notice.startAt || new Date(notice.startAt) <= now
+ const validEnd = !notice.endAt || new Date(notice.endAt) >= now
+ if (!(validStart && validEnd)) continue
+
+ // '다시 보지 않기' 설정 확인 (영구 설정만 확인)
+ const dontShowNever = isNoticeDontShowValid({
+ noticeId: notice.id,
+ duration: 'never'
+ })
+
+ // '다시 보지 않기' 설정이 없고, 현재 유효한 팝업 공지사항이면 강제 모달 표시
+ if (!dontShowNever) {
+ setForceModalNotice(notice)
+ setIsForceModalOpen(true)
+ setForceModalDontShow(false)
+ break // 첫 번째 강제 모달 대상만 처리
+ }
+ }
+ }, [])
+
+ // 강제 모달 닫기 핸들러
+ const handleForceModalClose = React.useCallback(() => {
+ if (forceModalDontShow && forceModalNotice) {
+ // '다시 보지 않기' 체크했으면 설정 저장 (영구로 설정)
+ setNoticeDontShow({
+ noticeId: forceModalNotice.id,
+ duration: 'never'
+ })
+ toast.success("설정이 저장되었습니다.")
+ }
+
+ setIsForceModalOpen(false)
+ setForceModalNotice(null)
+ setForceModalDontShow(false)
+ }, [forceModalDontShow, forceModalNotice])
+
+ // 강제 모달에서 '다시 보지 않기' 체크박스 변경 핸들러
+ const handleForceModalDontShowChange = React.useCallback((checked: boolean) => {
+ setForceModalDontShow(checked)
+ }, [])
+
// 다이얼로그 열기
const handleDialogOpen = (open: boolean) => {
setIsOpen(open)
@@ -408,6 +478,108 @@ export function InformationButton({
/>
)}
+ {/* 강제 모달 공지사항 다이얼로그 */}
+ <Dialog open={isForceModalOpen} onOpenChange={handleForceModalClose}>
+ <DialogContent className="max-w-2xl max-h-[80vh]">
+ <DialogHeader>
+ <div className="flex items-center gap-2">
+ <AlertCircle className="h-5 w-5 text-blue-500" />
+ <DialogTitle className="text-xl">
+ 공지사항
+ </DialogTitle>
+ <Badge variant="outline">
+ 필독
+ </Badge>
+ </div>
+ <DialogDescription>
+ 중요한 공지사항을 확인해주세요.
+ </DialogDescription>
+ </DialogHeader>
+
+ {forceModalNotice && (
+ <div className="space-y-4">
+ {/* 공지사항 정보 헤더 */}
+ <div className="bg-muted/50 rounded-lg p-4">
+ <div className="flex items-start justify-between">
+ <div className="flex-1">
+ <h3 className="font-semibold text-lg mb-2">
+ {forceModalNotice.title}
+ </h3>
+ <div className="flex items-center gap-4 text-sm text-muted-foreground">
+ <div className="flex items-center gap-1">
+ <User className="h-4 w-4" />
+ {forceModalNotice.authorName || "알 수 없음"}
+ </div>
+ <div className="flex items-center gap-1">
+ <Calendar className="h-4 w-4" />
+ {formatDate(forceModalNotice.createdAt, "KR")}
+ </div>
+ {forceModalNotice.pagePath && (
+ <Badge variant="secondary" className="text-xs">
+ {forceModalNotice.pagePath}
+ </Badge>
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <Separator />
+
+ {/* 공지사항 내용 */}
+ <ScrollArea className="h-[300px] w-full">
+ <div
+ className="prose prose-sm max-w-none"
+ dangerouslySetInnerHTML={{
+ __html: forceModalNotice.content || ""
+ }}
+ />
+ </ScrollArea>
+
+ {/* 유효기간 정보 */}
+ {(forceModalNotice.startAt || forceModalNotice.endAt) && (
+ <>
+ <Separator />
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
+ <Clock className="h-4 w-4" />
+ <span>유효기간:</span>
+ {forceModalNotice.startAt && (
+ <span>{formatDate(forceModalNotice.startAt, "KR")}</span>
+ )}
+ {forceModalNotice.startAt && forceModalNotice.endAt && <span> ~ </span>}
+ {forceModalNotice.endAt && (
+ <span>{formatDate(forceModalNotice.endAt, "KR")}</span>
+ )}
+ </div>
+ </>
+ )}
+
+ {/* '다시 보지 않기' 설정 */}
+ <div className="flex items-center space-x-2 p-4 bg-muted/30 rounded-lg">
+ <Checkbox
+ id="forceModalDontShow"
+ checked={forceModalDontShow}
+ onCheckedChange={handleForceModalDontShowChange}
+ />
+ <label
+ htmlFor="forceModalDontShow"
+ className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
+ >
+ 다시 보지 않기
+ </label>
+ </div>
+ </div>
+ )}
+
+ {/* 하단 버튼들 */}
+ <div className="flex items-center justify-end pt-4">
+ <Button onClick={handleForceModalClose}>
+ 확인
+ </Button>
+ </div>
+ </DialogContent>
+ </Dialog>
+
{/* PDFTron 뷰어 다이얼로그 - 주석 처리 (브라우저 내장 뷰어 사용) */}
{/* <PDFTronViewerDialog
open={viewerDialogOpen}