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);