diff --git a/src/app/announcements/page.tsx b/src/app/announcements/page.tsx index b3e169e..82b28de 100644 --- a/src/app/announcements/page.tsx +++ b/src/app/announcements/page.tsx @@ -17,7 +17,7 @@ function AnnouncementsContent() { const { isAuthenticated } = useAuthStore(); // URL 쿼리 파라미터에서 페이지 번호 가져오기 - const pageParam = searchParams.get('page'); + const pageParam = searchParams?.get('page'); const page = pageParam ? parseInt(pageParam) : 0; // 공지 사항 데이터 가져오기 diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 006b8e0..1bf25fe 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,13 +1,13 @@ "use client"; -import { useState, useEffect } from 'react'; +import { useState, useEffect, Suspense } from 'react'; import { useTheme } from '@/contexts/ThemeContext'; import { UserCircleIcon, KeyIcon, ArrowLeftIcon } from '@heroicons/react/24/outline'; import { useRouter, useSearchParams } from 'next/navigation'; import { useAuthStore, LoginRequest } from '@/lib/authStore'; import Link from 'next/link'; -export default function LoginPage() { +function LoginContent() { const { currentTheme } = useTheme(); const router = useRouter(); const searchParams = useSearchParams(); @@ -28,8 +28,8 @@ export default function LoginPage() { } = useAuthStore(); // silent 모드 확인 (내부 처리용) - const isSilentMode = searchParams.get('silent') === 'true'; - const callbackUrl = searchParams.get('callbackUrl'); + const isSilentMode = searchParams?.get('silent') === 'true'; + const callbackUrl = searchParams?.get('callbackUrl'); // 페이지 로드 시 인증 상태 확인 useEffect(() => { @@ -201,4 +201,12 @@ export default function LoginPage() { ); -} \ No newline at end of file +} + +export default function LoginPage() { + return ( + + + + ); +} \ No newline at end of file diff --git a/src/app/monitoring/page.tsx b/src/app/monitoring/page.tsx index 7dbccb7..ae345cc 100644 --- a/src/app/monitoring/page.tsx +++ b/src/app/monitoring/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useState, useRef } from 'react'; +import { useEffect, useState, useRef, useCallback, Suspense } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; import { useAuthStore } from '@/lib/authStore'; import { useTheme } from '@/contexts/ThemeContext'; @@ -276,7 +276,7 @@ interface RouteGroup { points: { lat: number; lng: number }[]; } -export default function MonitoringPage() { +function MonitoringContent() { const router = useRouter(); const searchParams = useSearchParams(); const { isAuthenticated } = useAuthStore(); @@ -406,12 +406,28 @@ export default function MonitoringPage() { }; }, []); + const handleSubscribe = useCallback(() => { + if (wsRef.current && companyId && wsConnected) { + console.log('구독 요청 전송:', companyId); + + const subscribeMsg = JSON.stringify({ + type: 'subscribe', + companyId: companyId + }); + + wsRef.current.send(subscribeMsg); + } else { + console.warn('WebSocket이 연결되지 않았거나 회사 ID가 없습니다'); + setError('WebSocket이 연결되지 않았거나 회사 ID가 없습니다'); + } + }, [companyId, wsConnected]); + useEffect(() => { if (wsConnected && companyId) { console.log('WebSocket 연결됨, 자동 구독 시도:', companyId); handleSubscribe(); } - }, [wsConnected]); + }, [wsConnected, companyId, handleSubscribe]); const connectWebSocket = () => { try { @@ -543,11 +559,11 @@ export default function MonitoringPage() { useEffect(() => { updateRoutePoints(); - }, [currentPositions, showRoute]); + }, [currentPositions, showRoute, carLocations]); useEffect(() => { updateRoutePoints(); - }, [showRoute]); + }, [showRoute, currentPositions, carLocations]); const visibleMarkers = currentPositions .filter(pos => showAllCars || selectedCars.includes(pos.carId)) @@ -582,22 +598,6 @@ export default function MonitoringPage() { } }, [currentPositions, selectedCars, mapSettings.followVehicle]); - const handleSubscribe = () => { - if (wsRef.current && companyId && wsConnected) { - console.log('구독 요청 전송:', companyId); - - const subscribeMsg = JSON.stringify({ - type: 'subscribe', - companyId: companyId - }); - - wsRef.current.send(subscribeMsg); - } else { - console.warn('WebSocket이 연결되지 않았거나 회사 ID가 없습니다'); - setError('WebSocket이 연결되지 않았거나 회사 ID가 없습니다'); - } - }; - const handleRefresh = () => { if (wsRef.current) { wsRef.current.close(); @@ -771,4 +771,12 @@ export default function MonitoringPage() { ); +} + +export default function MonitoringPage() { + return ( + + + + ); } \ No newline at end of file diff --git a/src/app/permissions/[employeeId]/page.tsx b/src/app/permissions/[employeeId]/page.tsx index 8fc71fc..559e70c 100644 --- a/src/app/permissions/[employeeId]/page.tsx +++ b/src/app/permissions/[employeeId]/page.tsx @@ -56,7 +56,7 @@ export default function EmployeePermissionsPage() { const { currentTheme } = useTheme(); const params = useParams(); const router = useRouter(); - const employeeId = params.employeeId as string; + const employeeId = params?.employeeId as string; const { users, userPermissions, loadingPermissions, permissionsError, fetchUserPermissions, updateUserPermissions, savingPermissions, savePermissionsError, savePermissionsSuccess, fetchUser } = useUserStore(); const [employee, setEmployee] = useState(null); @@ -65,8 +65,45 @@ export default function EmployeePermissionsPage() { const [searchTerm, setSearchTerm] = useState(''); const [permissions, setPermissions] = useState([]); - // 직원 정보 가져오기 + // 권한을 그룹별로 정렬하는 함수 + const getGroupedPermissions = useCallback(() => { + const groups = PERMISSION_GROUPS.map(group => ({ + ...group, + permissions: permissions.filter(p => { + if (group.id === 'admin') return p.id === 'PERM_ADMIN'; + return p.id.startsWith(`PERM_${group.id.toUpperCase()}_`); + }) + })); + + return groups; + }, [permissions]); + + // 검색어에 따라 권한 필터링 + const filteredGroups = useMemo(() => { + const groupedPermissions = getGroupedPermissions(); + + if (!searchTerm) { + return groupedPermissions; + } + + const filtered = groupedPermissions.map(group => ({ + ...group, + permissions: group.permissions.filter(p => + p.name.toLowerCase().includes(searchTerm.toLowerCase()) || + p.description.toLowerCase().includes(searchTerm.toLowerCase()) + ) + })).filter(group => group.permissions.length > 0); + + return filtered; + }, [searchTerm, getGroupedPermissions]); + useEffect(() => { + if (!params?.employeeId) { + router.push('/employees'); + return; + } + + const employeeId = params.employeeId as string; const fetchEmployeeData = async () => { setLoadingEmployee(true); setEmployeeError(null); @@ -124,16 +161,20 @@ export default function EmployeePermissionsPage() { }; fetchEmployeeData(); - }, [employeeId, users, fetchUser]); + }, [params, router, users, fetchUser]); // 권한 정보 가져오기 useEffect(() => { + if (!params?.employeeId) return; + const employeeId = params.employeeId as string; if (employee) { fetchUserPermissions(employeeId); } - }, [employeeId, fetchUserPermissions, employee]); + }, [params, employee, fetchUserPermissions]); useEffect(() => { + if (!params?.employeeId) return; + const employeeId = params.employeeId as string; // 기본적으로 모든 권한을 isGranted가 false인 상태로 초기화 const allPermissionsCopy = ALL_PERMISSIONS.map(p => ({...p, isGranted: false})); @@ -165,7 +206,7 @@ export default function EmployeePermissionsPage() { } setPermissions(allPermissionsCopy); - }, [employeeId, userPermissions, employee]); + }, [params, userPermissions, employee]); // 권한 변경 처리 const handlePermissionChange = (permissionId: string, isGranted: boolean) => { @@ -189,38 +230,6 @@ export default function EmployeePermissionsPage() { await updateUserPermissions(employeeId, permissions); }; - // 권한을 그룹별로 정렬하는 함수 - const getGroupedPermissions = useCallback(() => { - const groups = PERMISSION_GROUPS.map(group => ({ - ...group, - permissions: permissions.filter(p => { - if (group.id === 'admin') return p.id === 'PERM_ADMIN'; - return p.id.startsWith(`PERM_${group.id.toUpperCase()}_`); - }) - })); - - return groups; - }, [permissions]); - - // 검색어에 따라 권한 필터링 - const filteredGroups = useMemo(() => { - const groupedPermissions = getGroupedPermissions(); - - if (!searchTerm) { - return groupedPermissions; - } - - const filtered = groupedPermissions.map(group => ({ - ...group, - permissions: group.permissions.filter(p => - p.name.toLowerCase().includes(searchTerm.toLowerCase()) || - p.description.toLowerCase().includes(searchTerm.toLowerCase()) - ) - })).filter(group => group.permissions.length > 0); - - return filtered; - }, [searchTerm, getGroupedPermissions]); - // 뒤로가기 함수 const handleGoBack = () => { router.back(); diff --git a/src/components/common/CompanyDetailPanel.tsx b/src/components/common/CompanyDetailPanel.tsx index 0d9867c..0777c2e 100644 --- a/src/components/common/CompanyDetailPanel.tsx +++ b/src/components/common/CompanyDetailPanel.tsx @@ -29,8 +29,8 @@ export interface Company { interface CompanyDetailPanelProps { isOpen: boolean; onClose: () => void; - company: Company | null; - onUpdate?: (company: Company) => void; + company: Company; + onUpdate: (company: Company) => void; } export default function CompanyDetailPanel({ diff --git a/src/components/common/EmployeeList.tsx b/src/components/common/EmployeeList.tsx index 9c32a9a..89e7a43 100644 --- a/src/components/common/EmployeeList.tsx +++ b/src/components/common/EmployeeList.tsx @@ -18,7 +18,7 @@ export interface Employee { } interface EmployeeListProps { - // 사용하지 않는 employees props 제거 + // 필요한 props가 있다면 여기에 추가 } export default function EmployeeList(props: EmployeeListProps) { diff --git a/src/components/layout/PageLayout.tsx b/src/components/layout/PageLayout.tsx index 1ff39a4..1dc55c6 100644 --- a/src/components/layout/PageLayout.tsx +++ b/src/components/layout/PageLayout.tsx @@ -19,7 +19,7 @@ export default function PageLayout({ children }: PageLayoutProps) { const hideSidebarPaths = ['/', '/login', '/register']; // 사이드바를 표시할지 여부 결정 - const showSidebar = !hideSidebarPaths.includes(pathname); + const showSidebar = pathname ? !hideSidebarPaths.includes(pathname) : true; // 인증 인터셉터 설정 useEffect(() => { diff --git a/src/components/logs/VehicleLogDetailSlidePanel.tsx b/src/components/logs/VehicleLogDetailSlidePanel.tsx index 24b7f5b..7253a60 100644 --- a/src/components/logs/VehicleLogDetailSlidePanel.tsx +++ b/src/components/logs/VehicleLogDetailSlidePanel.tsx @@ -23,11 +23,15 @@ interface RoutePoint { timestamp: string; } -interface RouteResponse { +interface RouteData { mdn: string; route: RoutePoint[]; } +interface RouteResponse { + data: RouteData; +} + export default function VehicleLogDetailSlidePanel({ isOpen, onClose, log, onDelete, onUpdate }: VehicleLogDetailSlidePanelProps) { const { currentTheme } = useTheme(); const [isEditing, setIsEditing] = useState(false);