summaryrefslogtreecommitdiff
path: root/components/data-table/use-table-presets.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'components/data-table/use-table-presets.tsx')
-rw-r--r--components/data-table/use-table-presets.tsx338
1 files changed, 338 insertions, 0 deletions
diff --git a/components/data-table/use-table-presets.tsx b/components/data-table/use-table-presets.tsx
new file mode 100644
index 00000000..5e641762
--- /dev/null
+++ b/components/data-table/use-table-presets.tsx
@@ -0,0 +1,338 @@
+// hooks/use-table-presets.ts
+import { TableSettings } from "@/db/schema"
+import { useSession } from "next-auth/react"
+import { useCallback, useEffect, useState } from "react"
+import { toast } from "sonner"
+import useSWR from "swr"
+import { useSearchParams } from "next/navigation"
+
+interface TablePreset {
+ id: string
+ userId: string
+ tableId: string
+ name: string
+ settings: TableSettings
+ isDefault: boolean
+ isActive: boolean
+ isShared: boolean
+ createdAt: Date
+ updatedAt: Date
+}
+
+export function useTablePresets<TData>(
+ tableId: string,
+ initialSettings: TableSettings<TData>
+) {
+ const { data: session } = useSession()
+ const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false)
+ const [isClient, setIsClient] = useState(false)
+ const searchParams = useSearchParams()
+
+ // 클라이언트 마운트 확인
+ useEffect(() => {
+ setIsClient(true)
+ }, [])
+
+ // SWR을 사용한 데이터 페칭
+ const {
+ data: presets,
+ error,
+ mutate,
+ isLoading
+ } = useSWR<TablePreset[]>(
+ session?.user ? `/api/table-presets?tableId=${tableId}` : null,
+ (url: string) => fetch(url).then(res => res.json()),
+ {
+ revalidateOnFocus: false,
+ revalidateOnReconnect: true,
+ }
+ )
+
+ const activePreset = presets?.find(p => p.isActive)
+
+ // 현재 설정 가져오기 (URL + 활성 프리셋의 클라이언트 상태)
+ const getCurrentSettings = useCallback((): TableSettings<TData> => {
+ // 서버 렌더링 중이면 initialSettings 사용
+ if (!isClient || typeof window === 'undefined') {
+ return initialSettings
+ }
+
+ // 클라이언트에서만 URL 파라미터 읽기
+ const urlSettings = {
+ page: parseInt(searchParams.get('page') || '1'),
+ perPage: parseInt(searchParams.get('perPage') || '10'),
+ sort: JSON.parse(searchParams.get('sort') || JSON.stringify(initialSettings.sort)),
+ filters: JSON.parse(searchParams.get('filters') || '[]'),
+ joinOperator: (searchParams.get('joinOperator') as "and" | "or") || "and",
+ basicFilters: JSON.parse(searchParams.get('basicFilters') || '[]'),
+ basicJoinOperator: (searchParams.get('basicJoinOperator') as "and" | "or") || "and",
+ search: searchParams.get('search') || '',
+ from: searchParams.get('from') || undefined,
+ to: searchParams.get('to') || undefined,
+ }
+
+ // 활성 프리셋의 클라이언트 설정 병합
+ if (activePreset) {
+ return {
+ ...urlSettings,
+ columnVisibility: activePreset.settings.columnVisibility || {},
+ columnOrder: activePreset.settings.columnOrder || [],
+ pinnedColumns: activePreset.settings.pinnedColumns || { left: [], right: [] },
+ groupBy: activePreset.settings.groupBy || [],
+ expandedRows: activePreset.settings.expandedRows || []
+ }
+ }
+
+ return urlSettings
+ }, [activePreset, initialSettings, isClient, searchParams])
+
+ // 프리셋 생성
+ const createPreset = useCallback(async (
+ name: string,
+ settings: TableSettings<TData>,
+ isDefault = false
+ ) => {
+ try {
+ const response = await fetch('/api/table-presets', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ tableId,
+ name,
+ settings,
+ isDefault
+ })
+ })
+
+ if (!response.ok) {
+ throw new Error('Failed to create preset')
+ }
+
+ await mutate()
+ toast.success(`프리셋 '${name}'이 저장되었습니다`)
+ return true
+ } catch (error) {
+ console.error('Error creating preset:', error)
+ toast.error('프리셋 저장 중 오류가 발생했습니다')
+ return false
+ }
+ }, [tableId, mutate])
+
+ // 프리셋 적용
+ const applyPreset = useCallback(async (presetId: string) => {
+ try {
+ // 이전 활성 프리셋 비활성화 (있는 경우)
+ if (activePreset && activePreset.id !== presetId) {
+ await fetch(`/api/table-presets/${activePreset.id}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ isActive: false })
+ })
+ }
+
+ // 새 프리셋 활성화
+ await fetch(`/api/table-presets/${presetId}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ isActive: true })
+ })
+
+ const preset = presets?.find(p => p.id === presetId)
+ if (!preset) return false
+
+ // 클라이언트에서만 URL 업데이트
+ if (isClient && typeof window !== 'undefined') {
+ const params = new URLSearchParams()
+ if (preset.settings.page !== 1) params.set('page', preset.settings.page.toString())
+ if (preset.settings.perPage !== 10) params.set('perPage', preset.settings.perPage.toString())
+ if (preset.settings.sort && preset.settings.sort.length > 0) params.set('sort', JSON.stringify(preset.settings.sort))
+ if (preset.settings.filters && preset.settings.filters.length > 0) params.set('filters', JSON.stringify(preset.settings.filters))
+ if (preset.settings.joinOperator !== 'and') params.set('joinOperator', preset.settings.joinOperator)
+ if (preset.settings.basicFilters && preset.settings.basicFilters.length > 0) params.set('basicFilters', JSON.stringify(preset.settings.basicFilters))
+ if (preset.settings.basicJoinOperator !== 'and') params.set('basicJoinOperator', preset.settings.basicJoinOperator)
+ if (preset.settings.search) params.set('search', preset.settings.search)
+ if (preset.settings.from) params.set('from', preset.settings.from)
+ if (preset.settings.to) params.set('to', preset.settings.to)
+
+ const url = window.location.pathname + '?' + params.toString()
+ window.history.pushState({}, '', url)
+ }
+
+ await mutate()
+
+ // Next.js App Router의 경우 router.push나 window.location.reload 사용
+ if (isClient && typeof window !== 'undefined') {
+ window.location.reload()
+ }
+
+ return true
+ } catch (error) {
+ console.error('Error applying preset:', error)
+ toast.error('프리셋 적용 중 오류가 발생했습니다')
+ return false
+ }
+ }, [activePreset, presets, mutate, isClient])
+
+ // 프리셋 업데이트
+ const updatePreset = useCallback(async (
+ presetId: string,
+ settings: TableSettings<TData>
+ ) => {
+ try {
+ const response = await fetch(`/api/table-presets/${presetId}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ settings })
+ })
+
+ if (!response.ok) {
+ throw new Error('Failed to update preset')
+ }
+
+ await mutate()
+ const preset = presets?.find(p => p.id === presetId)
+ toast.success(`${preset?.name || '프리셋'}이 업데이트되었습니다`)
+ } catch (error) {
+ console.error('Error updating preset:', error)
+ toast.error('프리셋 업데이트 중 오류가 발생했습니다')
+ }
+ }, [presets, mutate])
+
+ // 프리셋 삭제
+ const deletePreset = useCallback(async (presetId: string) => {
+ try {
+ const response = await fetch(`/api/table-presets/${presetId}`, {
+ method: 'DELETE'
+ })
+
+ if (!response.ok) {
+ throw new Error('Failed to delete preset')
+ }
+
+ // 삭제할 프리셋이 활성 프리셋인 경우 다른 프리셋을 활성화
+ if (activePreset?.id === presetId && presets && presets.length > 1) {
+ const defaultPreset = presets.find(p => p.isDefault && p.id !== presetId) ||
+ presets.find(p => p.id !== presetId)
+ if (defaultPreset) {
+ await applyPreset(defaultPreset.id)
+ return true
+ }
+ }
+
+ await mutate()
+ const preset = presets?.find(p => p.id === presetId)
+ toast.success(`프리셋 '${preset?.name}'이 삭제되었습니다`)
+ return true
+ } catch (error) {
+ console.error('Error deleting preset:', error)
+ toast.error('프리셋 삭제 중 오류가 발생했습니다')
+ return false
+ }
+ }, [activePreset, presets, mutate, applyPreset])
+
+ // 기본 프리셋 설정
+ const setDefaultPreset = useCallback(async (presetId: string) => {
+ try {
+ // 기존 기본 프리셋 해제
+ const defaultPreset = presets?.find(p => p.isDefault)
+ if (defaultPreset && defaultPreset.id !== presetId) {
+ await fetch(`/api/table-presets/${defaultPreset.id}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ isDefault: false })
+ })
+ }
+
+ // 새 기본 프리셋 설정
+ await fetch(`/api/table-presets/${presetId}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ isDefault: true })
+ })
+
+ await mutate()
+ toast.success('기본 프리셋이 변경되었습니다')
+ } catch (error) {
+ console.error('Error setting default preset:', error)
+ toast.error('기본 프리셋 설정 중 오류가 발생했습니다')
+ }
+ }, [presets, mutate])
+
+ // 프리셋 이름 변경
+ const renamePreset = useCallback(async (presetId: string, newName: string) => {
+ try {
+ if (!newName.trim()) {
+ toast.error('프리셋 이름을 입력해주세요')
+ return false
+ }
+
+ const response = await fetch(`/api/table-presets/${presetId}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ name: newName.trim() })
+ })
+
+ if (!response.ok) {
+ const error = await response.json()
+ throw new Error(error.message || 'Failed to rename preset')
+ }
+
+ await mutate()
+ toast.success('프리셋 이름이 변경되었습니다')
+ return true
+ } catch (error) {
+ console.error('Error renaming preset:', error)
+ toast.error('프리셋 이름 변경 중 오류가 발생했습니다')
+ return false
+ }
+ }, [mutate])
+
+ // 클라이언트 상태 업데이트 (컬럼 가시성, 핀 등)
+ const updateClientState = useCallback(async (newClientState: Partial<TableSettings<TData>>) => {
+ if (!activePreset) return
+
+ const updatedSettings = {
+ ...activePreset.settings,
+ ...newClientState
+ }
+
+ await updatePreset(activePreset.id, updatedSettings)
+ }, [activePreset, updatePreset])
+
+ // URL 변경 감지 및 미저장 변경사항 체크
+ useEffect(() => {
+ if (!isClient || !presets || !activePreset) return
+
+ const currentSettings = getCurrentSettings()
+
+ // 현재 URL 설정과 활성 프리셋 설정 비교
+ const isSettingsChanged =
+ currentSettings.perPage !== activePreset.settings.perPage ||
+ JSON.stringify(currentSettings.sort) !== JSON.stringify(activePreset.settings.sort) ||
+ JSON.stringify(currentSettings.filters) !== JSON.stringify(activePreset.settings.filters) ||
+ currentSettings.joinOperator !== activePreset.settings.joinOperator ||
+ JSON.stringify(currentSettings.basicFilters) !== JSON.stringify(activePreset.settings.basicFilters) ||
+ currentSettings.basicJoinOperator !== activePreset.settings.basicJoinOperator ||
+ currentSettings.search !== activePreset.settings.search
+
+ setHasUnsavedChanges(isSettingsChanged)
+ }, [isClient, presets, activePreset, getCurrentSettings])
+
+ return {
+ // 상태
+ presets: presets || [],
+ activePresetId: activePreset?.id || null,
+ isLoading,
+ hasUnsavedChanges,
+
+ // 액션
+ createPreset,
+ applyPreset,
+ updatePreset,
+ deletePreset,
+ setDefaultPreset,
+ renamePreset,
+ updateClientState,
+ getCurrentSettings,
+ }
+} \ No newline at end of file