diff options
Diffstat (limited to 'components/data-table/use-table-presets.tsx')
| -rw-r--r-- | components/data-table/use-table-presets.tsx | 338 |
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 |
