import React, { useState, useEffect, useRef, createContext, useContext } from 'react'; import { HashRouter, Routes, Route } from 'react-router-dom'; import { Layout } from './components/layout/Layout'; import { HomePage } from './pages/HomePage'; import { IOMonitorPage } from './pages/IOMonitorPage'; import { RecipePage } from './pages/RecipePage'; import { HistoryPage } from './pages/HistoryPage'; import { SystemState, Recipe, IOPoint, LogEntry, RobotTarget, ConfigItem } from './types'; import { comms } from './communication'; import { AlertProvider, useAlert } from './contexts/AlertContext'; import { ThemeProvider } from './contexts/ThemeContext'; import { PickerMoveDialog } from './components/PickerMoveDialog'; // PickerMoveDialog 전역 상태 Context interface PickerMoveContextType { isPickerMoveOpen: boolean; openPickerMove: () => Promise; closePickerMove: () => void; } const PickerMoveContext = createContext(null); export const usePickerMove = () => { const context = useContext(PickerMoveContext); if (!context) { throw new Error('usePickerMove must be used within PickerMoveProvider'); } return context; }; // --- MOCK DATA --- const INITIAL_IO: IOPoint[] = [ ...Array.from({ length: 32 }, (_, i) => { let name = `DOUT_${i.toString().padStart(2, '0')}`; if (i === 0) name = "Tower Lamp Red"; if (i === 1) name = "Tower Lamp Yel"; if (i === 2) name = "Tower Lamp Grn"; return { id: i, name, type: 'output' as const, state: false }; }), ...Array.from({ length: 32 }, (_, i) => { let name = `DIN_${i.toString().padStart(2, '0')}`; let initialState = false; if (i === 0) name = "Front Door Sensor"; if (i === 1) name = "Right Door Sensor"; if (i === 2) name = "Left Door Sensor"; if (i === 3) name = "Back Door Sensor"; if (i === 4) { name = "Main Air Pressure"; initialState = true; } if (i === 5) { name = "Vacuum Generator"; initialState = true; } if (i === 6) { name = "Emergency Stop Loop"; initialState = true; } return { id: i, name, type: 'input' as const, state: initialState }; }) ]; // --- MAIN APP --- // 내부 App 컴포넌트 (AlertContext 내부에서 사용) function AppContent() { const [activeTab, setActiveTab] = useState<'recipe' | 'motion' | 'camera' | 'setting' | 'initialize' | null>(null); const [systemState, setSystemState] = useState(SystemState.IDLE); const [currentRecipe, setCurrentRecipe] = useState({ id: '0', name: 'No Recipe', lastModified: '-' }); const [robotTarget, setRobotTarget] = useState({ x: 0, y: 0, z: 0 }); const [logs, setLogs] = useState([]); const [ioPoints, setIoPoints] = useState([]); const [currentTime, setCurrentTime] = useState(new Date()); const [isLoading, setIsLoading] = useState(true); const [isHostConnected, setIsHostConnected] = useState(false); const [isPickerMoveOpen, setIsPickerMoveOpen] = useState(false); const videoRef = useRef(null); const { showAlert } = useAlert(); // PickerMoveDialog 열기 함수 (백엔드에서 openManage 호출) const openPickerMove = async (): Promise => { try { const result = await comms.openManage(); if (!result.success) { showAlert({ type: 'error', title: 'Cannot Open', message: result.message }); return false; } setIsPickerMoveOpen(true); return true; } catch (error: any) { showAlert({ type: 'error', title: 'Error', message: error.message || 'Failed to open manage dialog' }); return false; } }; // PickerMoveDialog 닫기 함수 const closePickerMove = async () => { setIsPickerMoveOpen(false); try { const result = await comms.closeManage(); if (result.shouldAutoInit) { const initResult = await comms.initializeDevice(); if (!initResult.success) { console.error('[App] Auto-init failed:', initResult.message); } } } catch (error) { console.error('[App] closeManage error:', error); } }; // -- COMMUNICATION LAYER -- useEffect(() => { const unsubscribe = comms.subscribe((msg: any) => { if (!msg) return; if (msg.type === 'CONNECTION_STATE') { setIsHostConnected(msg.connected); addLog(msg.connected ? "HOST CONNECTED" : "HOST DISCONNECTED", msg.connected ? "info" : "warning"); } // AUTO_OPEN_MANAGE 이벤트 처리 - 백엔드에서 IDLE 상태 진입 시 피커 이동 필요할 때 전송 if (msg.type === 'AUTO_OPEN_MANAGE') { console.log('[App] AUTO_OPEN_MANAGE event received:', msg.data?.reason); addLog(`AUTO MANAGE: ${msg.data?.reason || 'Picker move required'}`, 'warning'); // 이미 열려있지 않은 경우에만 열기 if (!isPickerMoveOpen) { openPickerMove(); } } if (msg.type === 'STATUS_UPDATE') { if (msg.position) { setRobotTarget({ x: msg.position.x, y: msg.position.y, z: msg.position.z }); } if (msg.ioState) { setIoPoints(prev => { const newIO = [...prev]; msg.ioState.forEach((update: { id: number, type: string, state: boolean }) => { const idx = newIO.findIndex(p => p.id === update.id && p.type === update.type); if (idx >= 0) newIO[idx] = { ...newIO[idx], state: update.state }; }); return newIO; }); } if (msg.sysState) { setSystemState(msg.sysState as SystemState); } } }); addLog("COMMUNICATION CHANNEL OPEN", "info"); setIsHostConnected(comms.getConnectionState()); const timer = setInterval(() => setCurrentTime(new Date()), 1000); return () => { clearInterval(timer); unsubscribe(); }; }, [isPickerMoveOpen]); // -- INITIALIZATION -- useEffect(() => { const initSystem = async () => { addLog("SYSTEM STARTED", "info"); // Initial IO data will be loaded by HomePage when it mounts try { const ioStr = await comms.getIOList(); const ioData = JSON.parse(ioStr); // Handle new structured format: { inputs: [...], outputs: [...], interlocks: [...] } let flatIoList: IOPoint[] = []; if (ioData.inputs && ioData.outputs) { // New format - flatten inputs and outputs flatIoList = [ ...ioData.outputs.map((io: any) => ({ id: io.id, name: io.name, type: 'output' as const, state: io.state })), ...ioData.inputs.map((io: any) => ({ id: io.id, name: io.name, type: 'input' as const, state: io.state })) ]; } else if (Array.isArray(ioData)) { // Old format - already flat array flatIoList = ioData; } setIoPoints(flatIoList); addLog("IO LIST LOADED", "info"); } catch (e) { addLog("FAILED TO LOAD IO DATA", "error"); } setIsLoading(false); }; initSystem(); }, []); const addLog = (msg: string, type: 'info' | 'warning' | 'error' = 'info') => { setLogs(prev => [{ id: Date.now() + Math.random(), timestamp: new Date().toLocaleTimeString(), message: msg, type }, ...prev].slice(0, 50)); }; // Logic Helpers const doorStates = { front: ioPoints.find(p => p.id === 0 && p.type === 'input')?.state || false, right: ioPoints.find(p => p.id === 1 && p.type === 'input')?.state || false, left: ioPoints.find(p => p.id === 2 && p.type === 'input')?.state || false, back: ioPoints.find(p => p.id === 3 && p.type === 'input')?.state || false, }; const isLowPressure = !(ioPoints.find(p => p.id === 4 && p.type === 'input')?.state ?? true); const isEmergencyStop = !(ioPoints.find(p => p.id === 6 && p.type === 'input')?.state ?? true); // -- COMMAND HANDLERS -- const handleControl = async (action: 'start' | 'stop' | 'reset') => { if (isEmergencyStop && action === 'start') return addLog('EMERGENCY STOP ACTIVE', 'error'); try { await comms.sendControl(action.toUpperCase()); addLog(`CMD SENT: ${action.toUpperCase()}`, 'info'); } catch (e) { addLog('COMM ERROR', 'error'); } }; const toggleIO = async (id: number, type: 'input' | 'output', forceState?: boolean) => { if (type === 'output') { const current = ioPoints.find(p => p.id === id && p.type === type)?.state; const nextState = forceState !== undefined ? forceState : !current; await comms.setIO(id, nextState); } }; const moveAxis = async (axis: 'X' | 'Y' | 'Z', value: number) => { if (isEmergencyStop) return; await comms.moveAxis(axis, value); addLog(`CMD MOVE ${axis}: ${value}`, 'info'); }; const handleSaveConfig = async (newConfig: ConfigItem[]) => { try { await comms.saveConfig(JSON.stringify(newConfig)); addLog("CONFIGURATION SAVED", "info"); } catch (e) { console.error(e); addLog("FAILED TO SAVE CONFIG", "error"); } }; const handleSelectRecipe = async (r: Recipe) => { try { addLog(`LOADING: ${r.name}`, 'info'); const result = await comms.selectRecipe(r.id); if (result.success) { setCurrentRecipe(r); addLog(`RECIPE LOADED: ${r.name}`, 'info'); } else { addLog(`RECIPE LOAD FAILED: ${result.message}`, 'error'); } } catch (error: any) { addLog(`RECIPE LOAD ERROR: ${error.message || 'Unknown error'}`, 'error'); console.error('Recipe selection error:', error); } }; const pickerMoveContextValue: PickerMoveContextType = { isPickerMoveOpen, openPickerMove, closePickerMove }; return ( setActiveTab(null)} videoRef={videoRef} /> } /> } /> } /> } /> {/* PickerMoveDialog - 전역에서 관리 */} ); } // 외부 App 컴포넌트 - ThemeProvider + AlertProvider로 감싸기 export default function App() { return ( ); }