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:
2025-11-27 00:14:47 +09:00
parent bb67d04d90
commit 3bd35ad852
19 changed files with 2917 additions and 81 deletions

View File

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