From 795b4915069c44f500a91638e16ded67b9e16618 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Tue, 1 Jul 2025 11:46:33 +0000 Subject: (최겸) 정보시스템 공지사항 개발 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/information/information-button.tsx | 306 +++++++++++------------ components/information/information-client.tsx | 340 ++++++++++++++++++++++++++ 2 files changed, 493 insertions(+), 153 deletions(-) create mode 100644 components/information/information-client.tsx (limited to 'components/information') diff --git a/components/information/information-button.tsx b/components/information/information-button.tsx index da0de548..38e8cb12 100644 --- a/components/information/information-button.tsx +++ b/components/information/information-button.tsx @@ -1,137 +1,129 @@ "use client" -import React, { useState, useEffect } from "react" -import { Info, Download, Edit } from "lucide-react" +import * as React from "react" +import { useState } from "react" import { Button } from "@/components/ui/button" + import { Dialog, DialogContent, - DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog" +import { Info, Download, Edit } from "lucide-react" import { getCachedPageInformation, getCachedEditPermission } from "@/lib/information/service" +import { getCachedPageNotices } from "@/lib/notice/service" import { UpdateInformationDialog } from "@/lib/information/table/update-information-dialog" +import { NoticeViewDialog } from "@/components/notice/notice-view-dialog" import type { PageInformation } from "@/db/schema/information" +import type { Notice } from "@/db/schema/notice" import { useSession } from "next-auth/react" +import { formatDate } from "@/lib/utils" interface InformationButtonProps { - pageCode: string + pagePath: string className?: string variant?: "default" | "outline" | "ghost" | "secondary" size?: "default" | "sm" | "lg" | "icon" } +type NoticeWithAuthor = Notice & { + authorName: string | null + authorEmail: string | null +} + export function InformationButton({ - pageCode, + pagePath, className, variant = "ghost", size = "icon" }: InformationButtonProps) { const { data: session } = useSession() - const [information, setInformation] = useState(null) - const [isLoading, setIsLoading] = useState(false) const [isOpen, setIsOpen] = useState(false) + const [information, setInformation] = useState(null) + const [notices, setNotices] = useState([]) const [hasEditPermission, setHasEditPermission] = useState(false) const [isEditDialogOpen, setIsEditDialogOpen] = useState(false) + const [selectedNotice, setSelectedNotice] = useState(null) + const [isNoticeViewDialogOpen, setIsNoticeViewDialogOpen] = useState(false) + const [dataLoaded, setDataLoaded] = useState(false) - useEffect(() => { - if (isOpen && !information) { - loadInformation() - } - }, [isOpen, information]) - - // 편집 권한 확인 - useEffect(() => { - const checkEditPermission = async () => { - if (session?.user?.id) { - try { - const permission = await getCachedEditPermission(pageCode, session.user.id) - setHasEditPermission(permission) - } catch (error) { - console.error("Failed to check edit permission:", error) - setHasEditPermission(false) - } - } - } + // 데이터 로드 함수 (단순화) + const loadData = React.useCallback(async () => { + if (dataLoaded) return // 이미 로드되었으면 중복 방지 - checkEditPermission() - }, [pageCode, session?.user?.id]) - - const loadInformation = async () => { - setIsLoading(true) try { - const data = await getCachedPageInformation(pageCode) - setInformation(data) + // pagePath 정규화 (앞의 / 제거) + const normalizedPath = pagePath.startsWith('/') ? pagePath.slice(1) : pagePath + + // 병렬로 데이터 조회 + const [infoResult, noticesResult] = await Promise.all([ + getCachedPageInformation(normalizedPath), + getCachedPageNotices(normalizedPath) + ]) + + setInformation(infoResult) + setNotices(noticesResult) + setDataLoaded(true) + + // 권한 확인 + if (session?.user?.id) { + const hasPermission = await getCachedEditPermission(normalizedPath, session.user.id) + setHasEditPermission(hasPermission) + } } catch (error) { - console.error("Failed to load information:", error) - } finally { - setIsLoading(false) + console.error("데이터 로딩 중 오류:", error) } - } + }, [pagePath, session?.user?.id, dataLoaded]) - const handleDownload = () => { - if (information?.attachmentFilePath && information?.attachmentFileName) { - const link = document.createElement('a') - link.href = information.attachmentFilePath - link.download = information.attachmentFileName - document.body.appendChild(link) - link.click() - document.body.removeChild(link) + // 다이얼로그 열기 + const handleDialogOpen = (open: boolean) => { + setIsOpen(open) + + if (open && !dataLoaded) { + loadData() } } + // 편집 관련 핸들러 const handleEditClick = () => { setIsEditDialogOpen(true) } - const handleEditClose = () => { + const handleEditSuccess = () => { setIsEditDialogOpen(false) - refreshInformation() + // 편집 후 데이터 다시 로드 + setDataLoaded(false) + loadData() + } + + // 공지사항 클릭 핸들러 + const handleNoticeClick = (notice: NoticeWithAuthor) => { + setSelectedNotice(notice) + setIsNoticeViewDialogOpen(true) } - const refreshInformation = () => { - // 편집 후 정보 다시 로드 - setInformation(null) - if (isOpen) { - loadInformation() + // 파일 다운로드 핸들러 + const handleDownload = () => { + if (information?.attachmentFilePath) { + window.open(information.attachmentFilePath, '_blank') } - // 캐시 무효화를 위해 다시 확인 - setTimeout(() => { - loadInformation() - }, 500) } - // 인포메이션이 없으면 버튼을 숨김 - const [hasInformation, setHasInformation] = useState(null) - useEffect(() => { - const checkInformation = async () => { - try { - const data = await getCachedPageInformation(pageCode) - setHasInformation(!!data) - } catch { - setHasInformation(false) - } - } - checkInformation() - }, [pageCode]) - // 인포메이션이 없으면 버튼을 숨김 - if (hasInformation === false) { - return null - } + return ( <> - + + {/* 첨부파일 */} + {information?.attachmentFileName && ( +
+

첨부파일

+
+
+
+
+ {information.attachmentFileName}
+ {information.attachmentFileSize && ( +
+ {information.attachmentFileSize} +
+ )}
- ) : ( -
- -

첨부된 파일이 없습니다.

-
- )} + +
- - ) : ( +
+ )} + + {!information && notices.length === 0 && (
- 이 페이지에 대한 정보가 없습니다. +

이 페이지에 대한 정보가 없습니다.

)}
+ {/* 공지사항 보기 다이얼로그 */} + + {/* 편집 다이얼로그 */} {information && ( )} diff --git a/components/information/information-client.tsx b/components/information/information-client.tsx new file mode 100644 index 00000000..513b8f20 --- /dev/null +++ b/components/information/information-client.tsx @@ -0,0 +1,340 @@ +"use client" + +import { useState, useEffect, useTransition } from "react" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { Badge } from "@/components/ui/badge" +import { + Search, + Edit, + FileText, + ChevronUp, + ChevronDown, + Download +} from "lucide-react" +import { toast } from "sonner" +import { formatDate } from "@/lib/utils" +import { getInformationLists } from "@/lib/information/service" +import type { PageInformation } from "@/db/schema/information" +import { UpdateInformationDialog } from "@/lib/information/table/update-information-dialog" + +interface InformationClientProps { + initialData?: PageInformation[] +} + +type SortField = "pageName" | "pagePath" | "createdAt" +type SortDirection = "asc" | "desc" + +export function InformationClient({ initialData = [] }: InformationClientProps) { + const [informations, setInformations] = useState(initialData) + const [loading, setLoading] = useState(false) + const [searchQuery, setSearchQuery] = useState("") + const [sortField, setSortField] = useState("createdAt") + const [sortDirection, setSortDirection] = useState("desc") + const [editingInformation, setEditingInformation] = useState(null) + const [isEditDialogOpen, setIsEditDialogOpen] = useState(false) + const [, startTransition] = useTransition() + + // 정보 목록 조회 + const fetchInformations = async () => { + try { + setLoading(true) + const search = searchQuery || undefined + + startTransition(async () => { + const result = await getInformationLists({ + page: 1, + perPage: 50, + search: search, + sort: [{ id: sortField, desc: sortDirection === "desc" }], + flags: [], + filters: [], + joinOperator: "and", + pagePath: "", + pageName: "", + informationContent: "", + isActive: null, + }) + + if (result?.data) { + setInformations(result.data) + } else { + toast.error("정보 목록을 가져오는데 실패했습니다.") + } + setLoading(false) + }) + } catch (error) { + console.error("Error fetching informations:", error) + toast.error("정보 목록을 가져오는데 실패했습니다.") + setLoading(false) + } + } + + // 검색 핸들러 + const handleSearch = () => { + fetchInformations() + } + + // 정렬 함수 + const sortInformations = (informations: PageInformation[]) => { + return [...informations].sort((a, b) => { + let aValue: string | Date + let bValue: string | Date + + if (sortField === "pageName") { + aValue = a.pageName + bValue = b.pageName + } else if (sortField === "pagePath") { + aValue = a.pagePath + bValue = b.pagePath + } else { + aValue = new Date(a.createdAt) + bValue = new Date(b.createdAt) + } + + if (aValue < bValue) { + return sortDirection === "asc" ? -1 : 1 + } + if (aValue > bValue) { + return sortDirection === "asc" ? 1 : -1 + } + return 0 + }) + } + + // 정렬 핸들러 + const handleSort = (field: SortField) => { + if (sortField === field) { + setSortDirection(sortDirection === "asc" ? "desc" : "asc") + } else { + setSortField(field) + setSortDirection("asc") + } + } + + // 편집 핸들러 + const handleEdit = (information: PageInformation) => { + setEditingInformation(information) + setIsEditDialogOpen(true) + } + + // 편집 완료 핸들러 + const handleEditClose = () => { + setIsEditDialogOpen(false) + setEditingInformation(null) + // 데이터 새로고침 + fetchInformations() + } + + // 다운로드 핸들러 + const handleDownload = (information: PageInformation) => { + if (information.attachmentFilePath && information.attachmentFileName) { + const link = document.createElement('a') + link.href = information.attachmentFilePath + link.download = information.attachmentFileName + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + } + } + + // 정렬된 정보 목록 + const sortedInformations = sortInformations(informations) + + useEffect(() => { + if (initialData.length > 0) { + setInformations(initialData) + } else { + fetchInformations() + } + }, []) + + useEffect(() => { + if (searchQuery !== "") { + fetchInformations() + } else if (initialData.length > 0) { + setInformations(initialData) + } + }, [searchQuery]) + + return ( +
+ {/* 검색 */} +
+
+
+ + setSearchQuery(e.target.value)} + className="pl-10" + onKeyPress={(e) => e.key === "Enter" && handleSearch()} + /> +
+ + +
+
+ + {/* 정보 테이블 */} +
+ + + + + + + + + + 정보 내용 + 첨부파일 + 상태 + + + + 작업 + + + + {loading ? ( + + + 로딩 중... + + + ) : informations.length === 0 ? ( + + + 정보가 없습니다. + + + ) : ( + sortedInformations.map((information) => ( + + +
+ + + {information.pageName} + +
+
+ + + {information.pagePath} + + + +
+ + + {information.attachmentFileName ? ( + + ) : ( + 없음 + )} + + + + {information.isActive ? "활성" : "비활성"} + + + + {formatDate(information.createdAt)} + + + + + + )) + )} + +
+
+ + {/* 편집 다이얼로그 */} + {editingInformation && ( + + )} +
+ ) +} \ No newline at end of file -- cgit v1.2.3