// 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( tableId: string, initialSettings: TableSettings ) { 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( 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 => { // 서버 렌더링 중이면 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, 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 ) => { 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>) => { if (!activePreset) return; const prev = activePreset.settings; const next = { ...prev, ...newClientState }; await updatePreset(activePreset.id, next); }, [activePreset, updatePreset, tableId] // ← tableId 도 의존성에 넣어 둡니다 ); // 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, } }