From 9f761849c2e98f650d089d00aed9df090497ada9 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
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<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}
-- 
cgit v1.2.3

