import { useState, useEffect } from 'react'; import { apiClient } from '../../../shared/api/client'; import { JSMpegPlayer } from '../components/JSMpegPlayer'; import { Plus, Settings, Trash2, X, Video } from 'lucide-react'; import { useAuth } from '../../../shared/auth/AuthContext'; import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors, type DragEndEvent } from '@dnd-kit/core'; import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable, rectSortingStrategy } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; interface Camera { id: number; name: string; ip_address: string; port: number; username?: string; password?: string; stream_path?: string; transport_mode?: 'tcp' | 'udp' | 'auto'; rtsp_encoding?: boolean; quality?: 'low' | 'medium' | 'original'; display_order?: number; is_active?: boolean | number; } // Wrap Camera Card with Sortable function SortableCamera({ camera, children, disabled }: { camera: Camera, children: React.ReactNode, disabled: boolean }) { const { attributes, listeners, setNodeRef, transform, transition, } = useSortable({ id: camera.id, disabled }); const style = { transform: CSS.Transform.toString(transform), transition, }; return (
{children}
); } export function MonitoringPage() { const { user } = useAuth(); const [cameras, setCameras] = useState([]); const [showForm, setShowForm] = useState(false); const [editingCamera, setEditingCamera] = useState(null); const [formData, setFormData] = useState>({ name: '', ip_address: '', port: 554, username: '', password: '', stream_path: '/stream1', transport_mode: 'tcp', rtsp_encoding: false, quality: 'low' }); const [loading, setLoading] = useState(false); const [streamVersions, setStreamVersions] = useState<{ [key: number]: number }>({}); const sensors = useSensors( useSensor(PointerSensor), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }) ); useEffect(() => { fetchCameras(); }, []); const fetchCameras = async () => { try { const res = await apiClient.get('/cameras'); setCameras(res.data); } catch (err) { console.error('Failed to fetch cameras', err); } }; const handleInputChange = (e: React.ChangeEvent) => { const { name, value } = e.target; setFormData(prev => ({ ...prev, [name]: value })); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); try { if (editingCamera) { await apiClient.put(`/cameras/${editingCamera.id}`, formData); // Force stream refresh for this camera setStreamVersions(prev => ({ ...prev, [editingCamera.id]: Date.now() })); } else { await apiClient.post('/cameras', formData); } setShowForm(false); setEditingCamera(null); setFormData({ name: '', ip_address: '', port: 554, username: '', password: '', stream_path: '/stream1', transport_mode: 'tcp', rtsp_encoding: false, quality: 'low' }); fetchCameras(); } catch (err) { console.error('Failed to save camera', err); const errMsg = (err as any).response?.data?.error || (err as any).message || '카메라 저장 실패'; alert(`오류 발생: ${errMsg}`); } finally { setLoading(false); } }; const handleEdit = (camera: Camera) => { setEditingCamera(camera); setFormData(camera); setShowForm(true); }; const handleDelete = async (id: number) => { if (!window.confirm('정말 삭제하시겠습니까?')) return; try { await apiClient.delete(`/cameras/${id}`); fetchCameras(); } catch (err) { console.error('Failed to delete camera', err); } }; const handleToggleStatus = async (camera: Camera) => { // Optimistic UI Update: Calculate new status const currentStatus = camera.is_active !== 0 && camera.is_active !== false; const newStatus = !currentStatus; // Immediately update UI state setCameras(prev => prev.map(c => c.id === camera.id ? { ...c, is_active: newStatus ? 1 : 0 } : c )); try { // Send request to server await apiClient.patch(`/cameras/${camera.id}/status`, { is_active: newStatus }); // Only silent fetch to sync eventually, no alert needed fetchCameras(); } catch (err) { console.error('Failed to toggle status', err); // Revert UI on failure setCameras(prev => prev.map(c => c.id === camera.id ? { ...c, is_active: currentStatus ? 1 : 0 } : c )); alert('상태 변경 실패 (되돌리기)'); } }; const handleDragEnd = async (event: DragEndEvent) => { const { active, over } = event; if (over && active.id !== over.id) { setCameras((items) => { const oldIndex = items.findIndex((item) => item.id === active.id); const newIndex = items.findIndex((item) => item.id === over.id); const newItems = arrayMove(items, oldIndex, newIndex); // Save new order apiClient.put('/cameras/reorder', { cameras: newItems.map(c => c.id) }) .catch(e => console.error('Failed to save order', e)); return newItems; }); } }; const getStreamUrl = (cameraId: number) => { // Use current host (proxied or direct) const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const version = streamVersions[cameraId] || 0; const url = `${protocol}//${window.location.host}/api/stream?cameraId=${cameraId}&v=${version}`; console.log(`[CCTV] Connecting to: ${url}`); return url; }; const handlePing = async (id: number) => { try { const res = await apiClient.get(`/cameras/${id}/ping`); // Critical check: If the server returns HTML (catch-all), it's old code if (typeof res.data === 'string' && res.data.includes(' { try { const res = await apiClient.get('/health'); console.log('[System] Server Version:', res.data); } catch (e) { console.warn('[System] Could not fetch server health - might be old version.'); } }; useEffect(() => { fetchCameras(); checkServerVersion(); }, []); return (

{(user?.role === 'admin' || user?.role === 'supervisor') && ( )}
{/* Camera Grid with DnD */} c.id)} strategy={rectSortingStrategy} >
{cameras.map(camera => (
{/* Overlay Controls */} {(user?.role === 'admin' || user?.role === 'supervisor') && (
e.stopPropagation()}>
)}

{camera.name} {(camera.is_active === 0 || camera.is_active === false) && (중지됨)}

{/* IP/Port Hidden as requested */}
))}
{cameras.length === 0 && (
)} {/* Add/Edit Modal */} {showForm && (

{editingCamera ? '카메라 설정 수정' : '새 카메라 추가'}

{/* Advanced Settings */}

고급 설정

)}
); }