From 9f761849c2e98f650d089d00aed9df090497ada9 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 27 Oct 2025 03:12:26 +0000 Subject: (최겸) 공지사항 팝업기능 및 다시보지않기 기능 구현(로컬 스토리지 활용) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/information/information-button.tsx | 182 +++++++++++++++++++++++++- 1 file changed, 177 insertions(+), 5 deletions(-) (limited to 'components/information/information-button.tsx') 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(null) + const [isForceModalOpen, setIsForceModalOpen] = useState(false) + const [forceModalDontShow, setForceModalDontShow] = useState(false) // const [viewerDialogOpen, setViewerDialogOpen] = useState(false) // 주석 처리 - 브라우저 내장 뷰어 사용 // const [selectedFile, setSelectedFile] = useState(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({ /> )} + {/* 강제 모달 공지사항 다이얼로그 */} + + + +
+ + + 공지사항 + + + 필독 + +
+ + 중요한 공지사항을 확인해주세요. + +
+ + {forceModalNotice && ( +
+ {/* 공지사항 정보 헤더 */} +
+
+
+

+ {forceModalNotice.title} +

+
+
+ + {forceModalNotice.authorName || "알 수 없음"} +
+
+ + {formatDate(forceModalNotice.createdAt, "KR")} +
+ {forceModalNotice.pagePath && ( + + {forceModalNotice.pagePath} + + )} +
+
+
+
+ + + + {/* 공지사항 내용 */} + +
+ + + {/* 유효기간 정보 */} + {(forceModalNotice.startAt || forceModalNotice.endAt) && ( + <> + +
+ + 유효기간: + {forceModalNotice.startAt && ( + {formatDate(forceModalNotice.startAt, "KR")} + )} + {forceModalNotice.startAt && forceModalNotice.endAt && ~ } + {forceModalNotice.endAt && ( + {formatDate(forceModalNotice.endAt, "KR")} + )} +
+ + )} + + {/* '다시 보지 않기' 설정 */} +
+ + +
+
+ )} + + {/* 하단 버튼들 */} +
+ +
+ +
+ {/* PDFTron 뷰어 다이얼로그 - 주석 처리 (브라우저 내장 뷰어 사용) */} {/*