diff options
Diffstat (limited to 'components/information')
| -rw-r--r-- | components/information/information-client.tsx | 212 |
1 files changed, 145 insertions, 67 deletions
diff --git a/components/information/information-client.tsx b/components/information/information-client.tsx index d863175f..50bc6a39 100644 --- a/components/information/information-client.tsx +++ b/components/information/information-client.tsx @@ -1,7 +1,8 @@ "use client"
-import { useState, useEffect, useTransition } from "react"
-import { useRouter } from "next/navigation"
+import React, { useState, useEffect, useTransition } from "react"
+import { useRouter, useParams } from "next/navigation"
+import { useTranslation } from "@/i18n/client"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import {
@@ -19,11 +20,13 @@ import { FileText,
ChevronUp,
ChevronDown,
- Download
+ Download,
+ Database,
+ RefreshCw
} from "lucide-react"
import { toast } from "sonner"
import { formatDate } from "@/lib/utils"
-import { getInformationLists } from "@/lib/information/service"
+import { getInformationLists, syncInformationFromMenuAssignments, getInformationDetail } from "@/lib/information/service"
import type { PageInformation } from "@/db/schema/information"
import { UpdateInformationDialog } from "@/lib/information/table/update-information-dialog"
@@ -36,6 +39,25 @@ type SortDirection = "asc" | "desc" export function InformationClient({ initialData = [] }: InformationClientProps) {
const router = useRouter()
+ const params = useParams()
+ const lng = (params?.lng as string) || 'ko'
+ const { t } = useTranslation(lng, 'menu')
+
+ // 안전한 번역 함수 (키가 없을 때 원본 키 반환)
+ const safeTranslate = (key: string): string => {
+ try {
+ const translated = t(key)
+ // 번역 키가 그대로 반환되는 경우 원본 키 사용
+ if (translated === key) {
+ return key
+ }
+ return translated || key
+ } catch (error) {
+ console.warn(`Translation failed for key: ${key}`, error)
+ return key
+ }
+ }
+
const [informations, setInformations] = useState<PageInformation[]>(initialData)
const [loading, setLoading] = useState(false)
const [searchQuery, setSearchQuery] = useState("")
@@ -43,28 +65,16 @@ export function InformationClient({ initialData = [] }: InformationClientProps) const [sortDirection, setSortDirection] = useState<SortDirection>("desc")
const [editingInformation, setEditingInformation] = useState<PageInformation | null>(null)
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false)
+ const [isSyncing, setIsSyncing] = 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,
- })
+ const result = await getInformationLists()
if (result?.data) {
setInformations(result.data)
@@ -80,9 +90,51 @@ export function InformationClient({ initialData = [] }: InformationClientProps) }
}
- // 검색 핸들러
+ // 클라이언트 사이드 필터링 및 정렬
+ const filteredAndSortedInformations = React.useMemo(() => {
+ let filtered = informations
+
+ // 검색 필터 (페이지명으로 검색)
+ if (searchQuery) {
+ filtered = filtered.filter(info =>
+ safeTranslate(info.pageName).toLowerCase().includes(searchQuery.toLowerCase()) ||
+ info.pagePath.toLowerCase().includes(searchQuery.toLowerCase())
+ )
+ }
+
+ // 정렬
+ filtered = filtered.sort((a, b) => {
+ let aValue: string | Date
+ let bValue: string | Date
+
+ switch (sortField) {
+ case "pageName":
+ aValue = safeTranslate(a.pageName)
+ bValue = safeTranslate(b.pageName)
+ break
+ case "pagePath":
+ aValue = a.pagePath
+ bValue = b.pagePath
+ break
+ case "createdAt":
+ aValue = new Date(a.createdAt)
+ bValue = new Date(b.createdAt)
+ break
+ default:
+ return 0
+ }
+
+ if (aValue < bValue) return sortDirection === "asc" ? -1 : 1
+ if (aValue > bValue) return sortDirection === "asc" ? 1 : -1
+ return 0
+ })
+
+ return filtered
+ }, [informations, searchQuery, sortField, sortDirection, safeTranslate])
+
+ // 검색 핸들러 (클라이언트 사이드에서 필터링하므로 별도 동작 불필요)
const handleSearch = () => {
- fetchInformations()
+ // 클라이언트 사이드 필터링이므로 별도 서버 요청 불필요
}
// 정렬 함수
@@ -92,8 +144,8 @@ export function InformationClient({ initialData = [] }: InformationClientProps) let bValue: string | Date
if (sortField === "pageName") {
- aValue = a.pageName
- bValue = b.pageName
+ aValue = safeTranslate(a.pageName)
+ bValue = safeTranslate(b.pageName)
} else if (sortField === "pagePath") {
aValue = a.pagePath
bValue = b.pagePath
@@ -123,9 +175,23 @@ export function InformationClient({ initialData = [] }: InformationClientProps) }
// 편집 핸들러
- const handleEdit = (information: PageInformation) => {
- setEditingInformation(information)
- setIsEditDialogOpen(true)
+ const handleEdit = async (information: PageInformation) => {
+ try {
+ // 첨부파일 정보까지 포함해서 가져오기
+ const detailData = await getInformationDetail(information.id)
+ if (detailData) {
+ setEditingInformation(detailData)
+ } else {
+ // 실패시 기본 정보라도 사용
+ setEditingInformation(information)
+ }
+ setIsEditDialogOpen(true)
+ } catch (error) {
+ console.error("Failed to load information detail:", error)
+ // 에러시 기본 정보라도 사용
+ setEditingInformation(information)
+ setIsEditDialogOpen(true)
+ }
}
// 편집 완료 핸들러
@@ -136,20 +202,48 @@ export function InformationClient({ initialData = [] }: InformationClientProps) 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)
+ // 다운로드 핸들러 (다중 첨부파일은 dialog에서 처리)
+ const handleDownload = async (information: PageInformation) => {
+ try {
+ // 첨부파일 정보까지 포함해서 가져오기
+ const detailData = await getInformationDetail(information.id)
+ if (detailData) {
+ setEditingInformation(detailData)
+ } else {
+ // 실패시 기본 정보라도 사용
+ setEditingInformation(information)
+ }
+ setIsEditDialogOpen(true)
+ } catch (error) {
+ console.error("Failed to load information detail:", error)
+ // 에러시 기본 정보라도 사용
+ setEditingInformation(information)
+ setIsEditDialogOpen(true)
+ }
+ }
+
+ // 메뉴 동기화 핸들러
+ const handleSync = async () => {
+ setIsSyncing(true)
+ try {
+ const result = await syncInformationFromMenuAssignments()
+
+ if (result.success) {
+ toast.success(result.message)
+ // 동기화 후 데이터 새로고침
+ fetchInformations()
+ } else {
+ toast.error(result.message)
+ }
+ } catch (error) {
+ console.error("동기화 오류:", error)
+ toast.error("메뉴 동기화 중 오류가 발생했습니다.")
+ } finally {
+ setIsSyncing(false)
}
}
- // 정렬된 정보 목록
- const sortedInformations = sortInformations(informations)
+
useEffect(() => {
if (initialData.length > 0) {
@@ -159,13 +253,7 @@ export function InformationClient({ initialData = [] }: InformationClientProps) }
}, [])
- useEffect(() => {
- if (searchQuery !== "") {
- fetchInformations()
- } else if (initialData.length > 0) {
- setInformations(initialData)
- }
- }, [searchQuery])
+ // searchQuery 변경 시 클라이언트 사이드 필터링으로 처리되므로 useEffect 제거
return (
<div className="space-y-6">
@@ -179,11 +267,19 @@ export function InformationClient({ initialData = [] }: InformationClientProps) value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10"
- onKeyPress={(e) => e.key === "Enter" && handleSearch()}
+ onKeyPress={(e) => e.key === "Enter"}
/>
</div>
- <Button onClick={handleSearch} variant="outline">
- 검색
+
+ <Button
+ variant="outline"
+ onClick={handleSync}
+ disabled={isSyncing}
+ className="gap-2"
+ >
+ <Database className={`h-4 w-4 ${isSyncing ? 'animate-pulse' : ''}`} />
+ <RefreshCw className={`h-4 w-4 ${isSyncing ? 'animate-spin' : ''}`} />
+ {isSyncing ? '동기화 중...' : '메뉴에서 동기화'}
</Button>
<Button
variant="outline"
@@ -230,7 +326,6 @@ export function InformationClient({ initialData = [] }: InformationClientProps) </button>
</TableHead>
<TableHead>정보 내용</TableHead>
- <TableHead>첨부파일</TableHead>
<TableHead>상태</TableHead>
<TableHead>
<button
@@ -257,20 +352,20 @@ export function InformationClient({ initialData = [] }: InformationClientProps) 로딩 중...
</TableCell>
</TableRow>
- ) : informations.length === 0 ? (
+ ) : filteredAndSortedInformations.length === 0 ? (
<TableRow>
<TableCell colSpan={7} className="text-center py-8 text-gray-500">
정보가 없습니다.
</TableCell>
</TableRow>
) : (
- sortedInformations.map((information) => (
+ filteredAndSortedInformations.map((information) => (
<TableRow key={information.id}>
<TableCell className="font-medium">
<div className="flex items-center gap-2">
<FileText className="h-4 w-4" />
<span className="max-w-[200px] truncate">
- {information.pageName}
+ {(information as any).translatedPageName || safeTranslate(information.pageName)}
</span>
</div>
</TableCell>
@@ -288,23 +383,6 @@ export function InformationClient({ initialData = [] }: InformationClientProps) />
</TableCell>
<TableCell>
- {information.attachmentFileName ? (
- <Button
- variant="outline"
- size="sm"
- onClick={() => handleDownload(information)}
- className="flex items-center gap-1"
- >
- <Download className="h-3 w-3" />
- <span className="max-w-[100px] truncate">
- {information.attachmentFileName}
- </span>
- </Button>
- ) : (
- <span className="text-gray-400">없음</span>
- )}
- </TableCell>
- <TableCell>
<Badge variant={information.isActive ? "default" : "secondary"}>
{information.isActive ? "활성" : "비활성"}
</Badge>
|
