feat: Add real-time IO/interlock updates, HW status display, and history page
- Implement real-time IO value updates via IOValueChanged event - Add interlock toggle and real-time interlock change events - Fix ToggleLight to check return value of DIO.SetRoomLight - Add HW status display in Footer matching WinForms HWState - Implement GetHWStatus API and 250ms broadcast interval - Create HistoryPage React component for work history viewing - Add GetHistoryData API for database queries - Add date range selection, search, filter, and CSV export - Add History button in Header navigation - Add PickerMoveDialog component for manage operations - Fix DataSet column names (idx, PRNATTACH, PRNVALID, qtymax) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
107
FrontEnd/App.tsx
107
FrontEnd/App.tsx
@@ -1,12 +1,31 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
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 } from './contexts/AlertContext';
|
||||
import { AlertProvider, useAlert } from './contexts/AlertContext';
|
||||
import { PickerMoveDialog } from './components/PickerMoveDialog';
|
||||
|
||||
// PickerMoveDialog 전역 상태 Context
|
||||
interface PickerMoveContextType {
|
||||
isPickerMoveOpen: boolean;
|
||||
openPickerMove: () => Promise<boolean>;
|
||||
closePickerMove: () => void;
|
||||
}
|
||||
|
||||
const PickerMoveContext = createContext<PickerMoveContextType | null>(null);
|
||||
|
||||
export const usePickerMove = () => {
|
||||
const context = useContext(PickerMoveContext);
|
||||
if (!context) {
|
||||
throw new Error('usePickerMove must be used within PickerMoveProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
// --- MOCK DATA ---
|
||||
|
||||
@@ -35,7 +54,8 @@ const INITIAL_IO: IOPoint[] = [
|
||||
|
||||
// --- MAIN APP ---
|
||||
|
||||
export default function App() {
|
||||
// 내부 App 컴포넌트 (AlertContext 내부에서 사용)
|
||||
function AppContent() {
|
||||
const [activeTab, setActiveTab] = useState<'recipe' | 'motion' | 'camera' | 'setting' | 'initialize' | null>(null);
|
||||
const [systemState, setSystemState] = useState<SystemState>(SystemState.IDLE);
|
||||
const [currentRecipe, setCurrentRecipe] = useState<Recipe>({ id: '0', name: 'No Recipe', lastModified: '-' });
|
||||
@@ -45,7 +65,49 @@ export default function App() {
|
||||
const [currentTime, setCurrentTime] = useState(new Date());
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isHostConnected, setIsHostConnected] = useState(false);
|
||||
const [isPickerMoveOpen, setIsPickerMoveOpen] = useState(false);
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const { showAlert } = useAlert();
|
||||
|
||||
// PickerMoveDialog 열기 함수 (백엔드에서 openManage 호출)
|
||||
const openPickerMove = async (): Promise<boolean> => {
|
||||
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(() => {
|
||||
@@ -57,6 +119,16 @@ export default function App() {
|
||||
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 });
|
||||
@@ -85,7 +157,7 @@ export default function App() {
|
||||
clearInterval(timer);
|
||||
unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
}, [isPickerMoveOpen]);
|
||||
|
||||
// -- INITIALIZATION --
|
||||
useEffect(() => {
|
||||
@@ -187,8 +259,14 @@ export default function App() {
|
||||
}
|
||||
};
|
||||
|
||||
const pickerMoveContextValue: PickerMoveContextType = {
|
||||
isPickerMoveOpen,
|
||||
openPickerMove,
|
||||
closePickerMove
|
||||
};
|
||||
|
||||
return (
|
||||
<AlertProvider>
|
||||
<PickerMoveContext.Provider value={pickerMoveContextValue}>
|
||||
<HashRouter>
|
||||
<Layout
|
||||
currentTime={currentTime}
|
||||
@@ -234,9 +312,28 @@ export default function App() {
|
||||
path="/recipe"
|
||||
element={<RecipePage />}
|
||||
/>
|
||||
<Route
|
||||
path="/history"
|
||||
element={<HistoryPage />}
|
||||
/>
|
||||
</Routes>
|
||||
</Layout>
|
||||
</HashRouter>
|
||||
|
||||
{/* PickerMoveDialog - 전역에서 관리 */}
|
||||
<PickerMoveDialog
|
||||
isOpen={isPickerMoveOpen}
|
||||
onClose={closePickerMove}
|
||||
/>
|
||||
</PickerMoveContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
// 외부 App 컴포넌트 - AlertProvider로 감싸기
|
||||
export default function App() {
|
||||
return (
|
||||
<AlertProvider>
|
||||
<AppContent />
|
||||
</AlertProvider>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user