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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -434,10 +434,10 @@ class CommunicationLayer {
|
||||
|
||||
// ===== VISION CONTROL METHODS =====
|
||||
|
||||
private async sendVisionCommand(command: string, responseType: string): Promise<{ success: boolean; message: string }> {
|
||||
private async sendVisionCommand(command: string, responseType: string, methodName: string): Promise<{ success: boolean; message: string }> {
|
||||
if (isWebView && machine) {
|
||||
// WebView2 mode - direct call to C# methods
|
||||
return { success: false, message: 'Vision commands not yet implemented in WebView2 mode' };
|
||||
const result = await (machine as any)[methodName]();
|
||||
return JSON.parse(result);
|
||||
} else {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.isConnected) {
|
||||
@@ -465,45 +465,46 @@ class CommunicationLayer {
|
||||
}
|
||||
|
||||
public async cameraConnect(): Promise<{ success: boolean; message: string }> {
|
||||
return this.sendVisionCommand('CAMERA_CONNECT', 'CAMERA_RESULT');
|
||||
return this.sendVisionCommand('CAMERA_CONNECT', 'CAMERA_RESULT', 'CameraConnect');
|
||||
}
|
||||
|
||||
public async cameraDisconnect(): Promise<{ success: boolean; message: string }> {
|
||||
return this.sendVisionCommand('CAMERA_DISCONNECT', 'CAMERA_RESULT');
|
||||
return this.sendVisionCommand('CAMERA_DISCONNECT', 'CAMERA_RESULT', 'CameraDisconnect');
|
||||
}
|
||||
|
||||
public async cameraGetImage(): Promise<{ success: boolean; message: string }> {
|
||||
return this.sendVisionCommand('CAMERA_GET_IMAGE', 'CAMERA_RESULT');
|
||||
return this.sendVisionCommand('CAMERA_GET_IMAGE', 'CAMERA_RESULT', 'CameraGetImage');
|
||||
}
|
||||
|
||||
public async cameraLiveView(): Promise<{ success: boolean; message: string }> {
|
||||
return this.sendVisionCommand('CAMERA_LIVE_VIEW', 'CAMERA_RESULT');
|
||||
return this.sendVisionCommand('CAMERA_LIVE_VIEW', 'CAMERA_RESULT', 'CameraLiveView');
|
||||
}
|
||||
|
||||
public async cameraReadTest(): Promise<{ success: boolean; message: string }> {
|
||||
return this.sendVisionCommand('CAMERA_READ_TEST', 'CAMERA_RESULT');
|
||||
return this.sendVisionCommand('CAMERA_READ_TEST', 'CAMERA_RESULT', 'CameraReadTest');
|
||||
}
|
||||
|
||||
public async keyenceTriggerOn(): Promise<{ success: boolean; message: string }> {
|
||||
return this.sendVisionCommand('KEYENCE_TRIGGER_ON', 'KEYENCE_RESULT');
|
||||
return this.sendVisionCommand('KEYENCE_TRIGGER_ON', 'KEYENCE_RESULT', 'KeyenceTriggerOn');
|
||||
}
|
||||
|
||||
public async keyenceTriggerOff(): Promise<{ success: boolean; message: string }> {
|
||||
return this.sendVisionCommand('KEYENCE_TRIGGER_OFF', 'KEYENCE_RESULT');
|
||||
return this.sendVisionCommand('KEYENCE_TRIGGER_OFF', 'KEYENCE_RESULT', 'KeyenceTriggerOff');
|
||||
}
|
||||
|
||||
public async keyenceGetImage(): Promise<{ success: boolean; message: string }> {
|
||||
return this.sendVisionCommand('KEYENCE_GET_IMAGE', 'KEYENCE_RESULT');
|
||||
return this.sendVisionCommand('KEYENCE_GET_IMAGE', 'KEYENCE_RESULT', 'KeyenceGetImage');
|
||||
}
|
||||
|
||||
public async keyenceSaveImage(): Promise<{ success: boolean; message: string }> {
|
||||
return this.sendVisionCommand('KEYENCE_SAVE_IMAGE', 'KEYENCE_RESULT');
|
||||
return this.sendVisionCommand('KEYENCE_SAVE_IMAGE', 'KEYENCE_RESULT', 'KeyenceSaveImage');
|
||||
}
|
||||
|
||||
// Light, Manual Print, Cancel Job commands
|
||||
public async toggleLight(): Promise<{ success: boolean; message: string }> {
|
||||
if (isWebView && machine) {
|
||||
return { success: false, message: 'Light control not yet implemented in WebView2 mode' };
|
||||
const result = await machine.ToggleLight();
|
||||
return JSON.parse(result);
|
||||
} else {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
@@ -537,7 +538,18 @@ class CommunicationLayer {
|
||||
count: number;
|
||||
}): Promise<{ success: boolean; message: string }> {
|
||||
if (isWebView && machine) {
|
||||
return { success: false, message: 'Manual print not yet implemented in WebView2 mode' };
|
||||
const result = await machine.ExecuteManualPrint(
|
||||
printData.sid,
|
||||
printData.venderLot,
|
||||
printData.qty,
|
||||
printData.mfg,
|
||||
printData.rid,
|
||||
printData.spy,
|
||||
printData.partNo,
|
||||
printData.printer,
|
||||
printData.count
|
||||
);
|
||||
return JSON.parse(result);
|
||||
} else {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
@@ -564,7 +576,8 @@ class CommunicationLayer {
|
||||
|
||||
public async cancelJob(): Promise<{ success: boolean; message: string }> {
|
||||
if (isWebView && machine) {
|
||||
return { success: false, message: 'Cancel job not yet implemented in WebView2 mode' };
|
||||
const result = await machine.CancelJob();
|
||||
return JSON.parse(result);
|
||||
} else {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
@@ -588,7 +601,8 @@ class CommunicationLayer {
|
||||
|
||||
public async openManage(): Promise<{ success: boolean; message: string }> {
|
||||
if (isWebView && machine) {
|
||||
return { success: false, message: 'Manage not yet implemented in WebView2 mode' };
|
||||
const result = await machine.OpenManage();
|
||||
return JSON.parse(result);
|
||||
} else {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
@@ -612,7 +626,8 @@ class CommunicationLayer {
|
||||
|
||||
public async openManual(): Promise<{ success: boolean; message: string }> {
|
||||
if (isWebView && machine) {
|
||||
return { success: false, message: 'Manual not yet implemented in WebView2 mode' };
|
||||
const result = await machine.OpenManual();
|
||||
return JSON.parse(result);
|
||||
} else {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
@@ -636,7 +651,8 @@ class CommunicationLayer {
|
||||
|
||||
public async openLogViewer(): Promise<{ success: boolean; message: string }> {
|
||||
if (isWebView && machine) {
|
||||
return { success: false, message: 'Log viewer not yet implemented in WebView2 mode' };
|
||||
const result = await machine.OpenLogViewer();
|
||||
return JSON.parse(result);
|
||||
} else {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
@@ -658,9 +674,10 @@ class CommunicationLayer {
|
||||
}
|
||||
}
|
||||
|
||||
private async openFolder(command: string): Promise<{ success: boolean; message: string }> {
|
||||
private async openFolder(command: string, methodName: string): Promise<{ success: boolean; message: string }> {
|
||||
if (isWebView && machine) {
|
||||
return { success: false, message: 'Folder open not yet implemented in WebView2 mode' };
|
||||
const result = await (machine as any)[methodName]();
|
||||
return JSON.parse(result);
|
||||
} else {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
@@ -683,19 +700,323 @@ class CommunicationLayer {
|
||||
}
|
||||
|
||||
public async openProgramFolder(): Promise<{ success: boolean; message: string }> {
|
||||
return this.openFolder('OPEN_PROGRAM_FOLDER');
|
||||
return this.openFolder('OPEN_PROGRAM_FOLDER', 'OpenProgramFolder');
|
||||
}
|
||||
|
||||
public async openLogFolder(): Promise<{ success: boolean; message: string }> {
|
||||
return this.openFolder('OPEN_LOG_FOLDER');
|
||||
return this.openFolder('OPEN_LOG_FOLDER', 'OpenLogFolder');
|
||||
}
|
||||
|
||||
public async openScreenshotFolder(): Promise<{ success: boolean; message: string }> {
|
||||
return this.openFolder('OPEN_SCREENSHOT_FOLDER');
|
||||
return this.openFolder('OPEN_SCREENSHOT_FOLDER', 'OpenScreenshotFolder');
|
||||
}
|
||||
|
||||
public async openSavedDataFolder(): Promise<{ success: boolean; message: string }> {
|
||||
return this.openFolder('OPEN_SAVED_DATA_FOLDER');
|
||||
return this.openFolder('OPEN_SAVED_DATA_FOLDER', 'OpenSavedDataFolder');
|
||||
}
|
||||
|
||||
// ===== PICKER MOVE METHODS =====
|
||||
|
||||
public async getPickerStatus(): Promise<void> {
|
||||
if (isWebView && machine) {
|
||||
const result = await machine.GetPickerStatus();
|
||||
this.notifyListeners({ type: 'PICKER_STATUS', data: JSON.parse(result) });
|
||||
} else {
|
||||
this.ws?.send(JSON.stringify({ type: 'GET_PICKER_STATUS' }));
|
||||
}
|
||||
}
|
||||
|
||||
private async sendPickerCommand(command: string, responseType: string, methodName: string): Promise<{ success: boolean; message: string }> {
|
||||
if (isWebView && machine) {
|
||||
const result = await (machine as any)[methodName]();
|
||||
return JSON.parse(result);
|
||||
} else {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.isConnected) {
|
||||
setTimeout(() => {
|
||||
if (!this.isConnected) reject({ success: false, message: "WebSocket connection timeout" });
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
this.listeners = this.listeners.filter(cb => cb !== handler);
|
||||
reject({ success: false, message: "Picker command timeout" });
|
||||
}, 10000);
|
||||
|
||||
const handler = (data: any) => {
|
||||
if (data.type === responseType) {
|
||||
clearTimeout(timeoutId);
|
||||
this.listeners = this.listeners.filter(cb => cb !== handler);
|
||||
resolve(data.data);
|
||||
}
|
||||
};
|
||||
this.listeners.push(handler);
|
||||
this.ws?.send(JSON.stringify({ type: command }));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async pickerMoveLeft(): Promise<{ success: boolean; message: string }> {
|
||||
return this.sendPickerCommand('PICKER_MOVE_LEFT', 'PICKER_RESULT', 'PickerMoveLeft');
|
||||
}
|
||||
|
||||
public async pickerMoveLeftWait(): Promise<{ success: boolean; message: string }> {
|
||||
return this.sendPickerCommand('PICKER_MOVE_LEFT_WAIT', 'PICKER_RESULT', 'PickerMoveLeftWait');
|
||||
}
|
||||
|
||||
public async pickerMoveCenter(): Promise<{ success: boolean; message: string }> {
|
||||
return this.sendPickerCommand('PICKER_MOVE_CENTER', 'PICKER_RESULT', 'PickerMoveCenter');
|
||||
}
|
||||
|
||||
public async pickerMoveRightWait(): Promise<{ success: boolean; message: string }> {
|
||||
return this.sendPickerCommand('PICKER_MOVE_RIGHT_WAIT', 'PICKER_RESULT', 'PickerMoveRightWait');
|
||||
}
|
||||
|
||||
public async pickerMoveRight(): Promise<{ success: boolean; message: string }> {
|
||||
return this.sendPickerCommand('PICKER_MOVE_RIGHT', 'PICKER_RESULT', 'PickerMoveRight');
|
||||
}
|
||||
|
||||
public async pickerJogStart(direction: 'up' | 'down' | 'left' | 'right'): Promise<{ success: boolean; message: string }> {
|
||||
if (isWebView && machine) {
|
||||
const result = await machine.PickerJogStart(direction);
|
||||
return JSON.parse(result);
|
||||
} else {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
this.listeners = this.listeners.filter(cb => cb !== handler);
|
||||
reject({ success: false, message: "Jog command timeout" });
|
||||
}, 5000);
|
||||
|
||||
const handler = (data: any) => {
|
||||
if (data.type === 'PICKER_JOG_RESULT') {
|
||||
clearTimeout(timeoutId);
|
||||
this.listeners = this.listeners.filter(cb => cb !== handler);
|
||||
resolve(data.data);
|
||||
}
|
||||
};
|
||||
this.listeners.push(handler);
|
||||
this.ws?.send(JSON.stringify({ type: 'PICKER_JOG_START', direction }));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async pickerJogStop(): Promise<{ success: boolean; message: string }> {
|
||||
return this.sendPickerCommand('PICKER_JOG_STOP', 'PICKER_JOG_RESULT', 'PickerJogStop');
|
||||
}
|
||||
|
||||
public async pickerStop(): Promise<{ success: boolean; message: string }> {
|
||||
return this.sendPickerCommand('PICKER_STOP', 'PICKER_RESULT', 'PickerStop');
|
||||
}
|
||||
|
||||
public async cancelVisionValidation(side: 'left' | 'right'): Promise<{ success: boolean; message: string }> {
|
||||
if (isWebView && machine) {
|
||||
const result = await machine.CancelVisionValidation(side);
|
||||
return JSON.parse(result);
|
||||
} else {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
this.listeners = this.listeners.filter(cb => cb !== handler);
|
||||
reject({ success: false, message: "Vision cancel timeout" });
|
||||
}, 5000);
|
||||
|
||||
const handler = (data: any) => {
|
||||
if (data.type === 'VISION_CANCEL_RESULT') {
|
||||
clearTimeout(timeoutId);
|
||||
this.listeners = this.listeners.filter(cb => cb !== handler);
|
||||
resolve(data.data);
|
||||
}
|
||||
};
|
||||
this.listeners.push(handler);
|
||||
this.ws?.send(JSON.stringify({ type: 'CANCEL_VISION_VALIDATION', side }));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async pickerManagePosition(side: 'left' | 'right'): Promise<{ success: boolean; message: string }> {
|
||||
if (isWebView && machine) {
|
||||
const result = await machine.PickerManagePosition(side);
|
||||
return JSON.parse(result);
|
||||
} else {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
this.listeners = this.listeners.filter(cb => cb !== handler);
|
||||
reject({ success: false, message: "Manage position timeout" });
|
||||
}, 30000); // Longer timeout for motion
|
||||
|
||||
const handler = (data: any) => {
|
||||
if (data.type === 'PICKER_MANAGE_RESULT') {
|
||||
clearTimeout(timeoutId);
|
||||
this.listeners = this.listeners.filter(cb => cb !== handler);
|
||||
resolve(data.data);
|
||||
}
|
||||
};
|
||||
this.listeners.push(handler);
|
||||
this.ws?.send(JSON.stringify({ type: 'PICKER_MANAGE_POSITION', side }));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async pickerManageReturn(): Promise<{ success: boolean; message: string }> {
|
||||
return this.sendPickerCommand('PICKER_MANAGE_RETURN', 'PICKER_MANAGE_RESULT', 'PickerManageReturn');
|
||||
}
|
||||
|
||||
public async pickerZHome(): Promise<{ success: boolean; message: string }> {
|
||||
return this.sendPickerCommand('PICKER_Z_HOME', 'PICKER_RESULT', 'PickerZHome');
|
||||
}
|
||||
|
||||
public async pickerZZero(): Promise<{ success: boolean; message: string }> {
|
||||
return this.sendPickerCommand('PICKER_Z_ZERO', 'PICKER_RESULT', 'PickerZZero');
|
||||
}
|
||||
|
||||
public async pickerTestPrint(side: 'left' | 'right'): Promise<{ success: boolean; message: string }> {
|
||||
if (isWebView && machine) {
|
||||
const result = await machine.PickerTestPrint(side);
|
||||
return JSON.parse(result);
|
||||
} else {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
this.listeners = this.listeners.filter(cb => cb !== handler);
|
||||
reject({ success: false, message: "Test print timeout" });
|
||||
}, 10000);
|
||||
|
||||
const handler = (data: any) => {
|
||||
if (data.type === 'PICKER_PRINT_RESULT') {
|
||||
clearTimeout(timeoutId);
|
||||
this.listeners = this.listeners.filter(cb => cb !== handler);
|
||||
resolve(data.data);
|
||||
}
|
||||
};
|
||||
this.listeners.push(handler);
|
||||
this.ws?.send(JSON.stringify({ type: 'PICKER_TEST_PRINT', side }));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async canCloseManage(): Promise<{ canClose: boolean; message: string }> {
|
||||
if (isWebView && machine) {
|
||||
const result = await machine.CanCloseManage();
|
||||
return JSON.parse(result);
|
||||
} else {
|
||||
return new Promise((resolve) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
this.listeners = this.listeners.filter(cb => cb !== handler);
|
||||
resolve({ canClose: true, message: '' });
|
||||
}, 5000);
|
||||
|
||||
const handler = (data: any) => {
|
||||
if (data.type === 'CAN_CLOSE_MANAGE_RESULT') {
|
||||
clearTimeout(timeoutId);
|
||||
this.listeners = this.listeners.filter(cb => cb !== handler);
|
||||
resolve(data.data);
|
||||
}
|
||||
};
|
||||
this.listeners.push(handler);
|
||||
this.ws?.send(JSON.stringify({ type: 'CAN_CLOSE_MANAGE' }));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Close manage dialog - backend handles flag clear and auto-init check
|
||||
public async closeManage(): Promise<{ shouldAutoInit: boolean }> {
|
||||
if (isWebView && machine) {
|
||||
const result = await machine.CloseManage();
|
||||
return JSON.parse(result);
|
||||
} else {
|
||||
return new Promise((resolve) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
this.listeners = this.listeners.filter(cb => cb !== handler);
|
||||
resolve({ shouldAutoInit: false });
|
||||
}, 5000);
|
||||
|
||||
const handler = (data: any) => {
|
||||
if (data.type === 'CLOSE_MANAGE_RESULT') {
|
||||
clearTimeout(timeoutId);
|
||||
this.listeners = this.listeners.filter(cb => cb !== handler);
|
||||
resolve(data.data);
|
||||
}
|
||||
};
|
||||
this.listeners.push(handler);
|
||||
this.ws?.send(JSON.stringify({ type: 'CLOSE_MANAGE' }));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ===== HISTORY DATA =====
|
||||
|
||||
// Get history data from database (fHistory.cs의 refreshList와 동일)
|
||||
public async getHistoryData(startDate: string, endDate: string, search: string): Promise<{ success: boolean; data: any[]; mcName?: string; message?: string }> {
|
||||
if (isWebView && machine) {
|
||||
const result = await machine.GetHistoryData(startDate, endDate, search);
|
||||
return JSON.parse(result);
|
||||
} else {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
this.listeners = this.listeners.filter(cb => cb !== handler);
|
||||
reject({ success: false, message: 'History data fetch timeout', data: [] });
|
||||
}, 30000); // 30 second timeout for potentially large data
|
||||
|
||||
const handler = (data: any) => {
|
||||
if (data.type === 'HISTORY_DATA_RESULT') {
|
||||
clearTimeout(timeoutId);
|
||||
this.listeners = this.listeners.filter(cb => cb !== handler);
|
||||
resolve(data.data);
|
||||
}
|
||||
};
|
||||
this.listeners.push(handler);
|
||||
this.ws?.send(JSON.stringify({ type: 'GET_HISTORY_DATA', startDate, endDate, search }));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ===== INTERLOCK METHODS =====
|
||||
|
||||
// Toggle interlock (DIOMonitor.cs의 gvILXF_ItemClick과 동일)
|
||||
public async toggleInterlock(axisIndex: number, lockIndex: number): Promise<{ success: boolean; newState?: boolean; message?: string }> {
|
||||
if (isWebView && machine) {
|
||||
const result = await machine.ToggleInterlock(axisIndex, lockIndex);
|
||||
return JSON.parse(result);
|
||||
} else {
|
||||
return new Promise((resolve) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
this.listeners = this.listeners.filter(cb => cb !== handler);
|
||||
resolve({ success: false, message: 'Timeout' });
|
||||
}, 5000);
|
||||
|
||||
const handler = (data: any) => {
|
||||
if (data.type === 'TOGGLE_INTERLOCK_RESULT') {
|
||||
clearTimeout(timeoutId);
|
||||
this.listeners = this.listeners.filter(cb => cb !== handler);
|
||||
resolve(data.data);
|
||||
}
|
||||
};
|
||||
this.listeners.push(handler);
|
||||
this.ws?.send(JSON.stringify({ type: 'TOGGLE_INTERLOCK', axisIndex, lockIndex }));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Get interlock list
|
||||
public async getInterlockList(): Promise<string> {
|
||||
if (isWebView && machine) {
|
||||
return await machine.GetInterlockList();
|
||||
} else {
|
||||
return new Promise((resolve) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
this.listeners = this.listeners.filter(cb => cb !== handler);
|
||||
resolve('[]');
|
||||
}, 5000);
|
||||
|
||||
const handler = (data: any) => {
|
||||
if (data.type === 'INTERLOCK_LIST_RESULT') {
|
||||
clearTimeout(timeoutId);
|
||||
this.listeners = this.listeners.filter(cb => cb !== handler);
|
||||
resolve(JSON.stringify(data.data));
|
||||
}
|
||||
};
|
||||
this.listeners.push(handler);
|
||||
this.ws?.send(JSON.stringify({ type: 'GET_INTERLOCK_LIST' }));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useState, useRef, useEffect } from 'react';
|
||||
import { Package, X, ChevronRight } from 'lucide-react';
|
||||
import { comms } from '../communication';
|
||||
import { useAlert } from '../contexts/AlertContext';
|
||||
import { usePickerMove } from '../App';
|
||||
|
||||
interface FunctionMenuProps {
|
||||
isOpen: boolean;
|
||||
@@ -12,6 +13,7 @@ export const FunctionMenu: React.FC<FunctionMenuProps> = ({ isOpen, onClose }) =
|
||||
const [activeSubmenu, setActiveSubmenu] = useState<string | null>(null);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const { showAlert } = useAlert();
|
||||
const { openPickerMove } = usePickerMove();
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
@@ -29,8 +31,6 @@ export const FunctionMenu: React.FC<FunctionMenuProps> = ({ isOpen, onClose }) =
|
||||
};
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleCommand = async (commandFn: () => Promise<{ success: boolean; message: string }>, actionName: string) => {
|
||||
try {
|
||||
const result = await commandFn();
|
||||
@@ -55,6 +55,8 @@ export const FunctionMenu: React.FC<FunctionMenuProps> = ({ isOpen, onClose }) =
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{isOpen && (
|
||||
<div
|
||||
ref={menuRef}
|
||||
className="absolute top-20 left-1/2 -translate-x-1/2 z-50 bg-black/95 backdrop-blur-md border border-neon-blue/50 rounded-lg shadow-glow-blue min-w-[300px]"
|
||||
@@ -77,7 +79,12 @@ export const FunctionMenu: React.FC<FunctionMenuProps> = ({ isOpen, onClose }) =
|
||||
<div className="p-2">
|
||||
{/* Manage */}
|
||||
<button
|
||||
onClick={() => handleCommand(() => comms.openManage(), 'Manage')}
|
||||
onClick={async () => {
|
||||
const success = await openPickerMove();
|
||||
if (success) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
className="w-full flex items-center justify-between px-4 py-3 text-left text-slate-300 hover:bg-neon-blue/10 hover:text-neon-blue rounded transition-colors font-tech"
|
||||
>
|
||||
<span>Manage</span>
|
||||
@@ -136,5 +143,7 @@ export const FunctionMenu: React.FC<FunctionMenuProps> = ({ isOpen, onClose }) =
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -28,7 +28,7 @@ export const InitializeModal: React.FC<InitializeModalProps> = ({ isOpen, onClos
|
||||
]);
|
||||
const [isInitializing, setIsInitializing] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const [statusInterval, setStatusInterval] = useState<NodeJS.Timeout | null>(null);
|
||||
const [statusInterval, setStatusInterval] = useState<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
// Reset state when modal closes
|
||||
useEffect(() => {
|
||||
|
||||
446
FrontEnd/components/PickerMoveDialog.tsx
Normal file
446
FrontEnd/components/PickerMoveDialog.tsx
Normal file
@@ -0,0 +1,446 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { X, Move, Square, ChevronLeft, ChevronRight, ChevronUp, ChevronDown, Home, Printer, XCircle, Settings } from 'lucide-react';
|
||||
import { comms } from '../communication';
|
||||
import { useAlert } from '../contexts/AlertContext';
|
||||
|
||||
interface PickerMoveDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface PickerStatus {
|
||||
xEnabled: boolean;
|
||||
zEnabled: boolean;
|
||||
pickerSafe: boolean;
|
||||
managementEnabled: boolean;
|
||||
manPosL: boolean;
|
||||
manPosR: boolean;
|
||||
}
|
||||
|
||||
export const PickerMoveDialog: React.FC<PickerMoveDialogProps> = ({ isOpen, onClose }) => {
|
||||
const { showAlert, showConfirm } = useAlert();
|
||||
const [status, setStatus] = useState<PickerStatus>({
|
||||
xEnabled: false,
|
||||
zEnabled: false,
|
||||
pickerSafe: false,
|
||||
managementEnabled: false,
|
||||
manPosL: false,
|
||||
manPosR: false
|
||||
});
|
||||
const [isJogging, setIsJogging] = useState(false);
|
||||
|
||||
// Subscribe to picker status updates
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
const unsubscribe = comms.subscribe((data: any) => {
|
||||
if (data.type === 'PICKER_STATUS') {
|
||||
setStatus(data.data);
|
||||
}
|
||||
});
|
||||
|
||||
// Request initial status
|
||||
comms.getPickerStatus();
|
||||
|
||||
// Set up polling interval
|
||||
const intervalId = setInterval(() => {
|
||||
comms.getPickerStatus();
|
||||
}, 200);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
const handleClose = async () => {
|
||||
try {
|
||||
const result = await comms.canCloseManage();
|
||||
if (!result.canClose) {
|
||||
showAlert({
|
||||
type: 'error',
|
||||
title: 'Cannot Close',
|
||||
message: result.message || 'Printer motion is in management position.\nReturn to position and try again.'
|
||||
});
|
||||
return;
|
||||
}
|
||||
onClose();
|
||||
} catch (error) {
|
||||
// If check fails, still allow close
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
// === Position Move Buttons ===
|
||||
const handleMoveLeft = async () => {
|
||||
const result = await comms.pickerMoveLeft();
|
||||
if (!result.success) {
|
||||
showAlert({ type: 'error', title: 'Move Failed', message: result.message });
|
||||
}
|
||||
};
|
||||
|
||||
const handleMoveLeftWait = async () => {
|
||||
const result = await comms.pickerMoveLeftWait();
|
||||
if (!result.success) {
|
||||
showAlert({ type: 'error', title: 'Move Failed', message: result.message });
|
||||
}
|
||||
};
|
||||
|
||||
const handleMoveCenter = async () => {
|
||||
const result = await comms.pickerMoveCenter();
|
||||
if (!result.success) {
|
||||
showAlert({ type: 'error', title: 'Move Failed', message: result.message });
|
||||
}
|
||||
};
|
||||
|
||||
const handleMoveRightWait = async () => {
|
||||
const result = await comms.pickerMoveRightWait();
|
||||
if (!result.success) {
|
||||
showAlert({ type: 'error', title: 'Move Failed', message: result.message });
|
||||
}
|
||||
};
|
||||
|
||||
const handleMoveRight = async () => {
|
||||
const result = await comms.pickerMoveRight();
|
||||
if (!result.success) {
|
||||
showAlert({ type: 'error', title: 'Move Failed', message: result.message });
|
||||
}
|
||||
};
|
||||
|
||||
// === Jog Control ===
|
||||
const handleJogStart = useCallback(async (direction: 'up' | 'down' | 'left' | 'right') => {
|
||||
setIsJogging(true);
|
||||
await comms.pickerJogStart(direction);
|
||||
}, []);
|
||||
|
||||
const handleJogStop = useCallback(async () => {
|
||||
setIsJogging(false);
|
||||
await comms.pickerJogStop();
|
||||
}, []);
|
||||
|
||||
const handleStop = async () => {
|
||||
await comms.pickerStop();
|
||||
};
|
||||
|
||||
// === Vision Validation Cancel ===
|
||||
const handleCancelVisionL = async () => {
|
||||
const confirmed = await showConfirm({
|
||||
title: 'Cancel Vision Validation',
|
||||
message: 'Do you want to cancel LEFT-QR code verification?'
|
||||
});
|
||||
if (confirmed) {
|
||||
const result = await comms.cancelVisionValidation('left');
|
||||
if (result.success) {
|
||||
showAlert({ type: 'success', title: 'Cancelled', message: 'LEFT-QR verification cancelled' });
|
||||
} else {
|
||||
showAlert({ type: 'error', title: 'Cancel Failed', message: result.message });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelVisionR = async () => {
|
||||
const confirmed = await showConfirm({
|
||||
title: 'Cancel Vision Validation',
|
||||
message: 'Do you want to cancel RIGHT-QR code verification?'
|
||||
});
|
||||
if (confirmed) {
|
||||
const result = await comms.cancelVisionValidation('right');
|
||||
if (result.success) {
|
||||
showAlert({ type: 'success', title: 'Cancelled', message: 'RIGHT-QR verification cancelled' });
|
||||
} else {
|
||||
showAlert({ type: 'error', title: 'Cancel Failed', message: result.message });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// === Management Position ===
|
||||
const handleManagePosL = async () => {
|
||||
const result = await comms.pickerManagePosition('left');
|
||||
if (!result.success) {
|
||||
showAlert({ type: 'error', title: 'Move Failed', message: result.message });
|
||||
}
|
||||
};
|
||||
|
||||
const handleManagePosR = async () => {
|
||||
const result = await comms.pickerManagePosition('right');
|
||||
if (!result.success) {
|
||||
showAlert({ type: 'error', title: 'Move Failed', message: result.message });
|
||||
}
|
||||
};
|
||||
|
||||
const handleManagePosReturn = async () => {
|
||||
const result = await comms.pickerManageReturn();
|
||||
if (!result.success) {
|
||||
showAlert({ type: 'error', title: 'Return Failed', message: result.message });
|
||||
}
|
||||
};
|
||||
|
||||
// === Z-Axis Control ===
|
||||
const handleZHome = async () => {
|
||||
const confirmed = await showConfirm({
|
||||
title: 'Z-Axis Home',
|
||||
message: 'Do you want to proceed with picker Z-axis home search?'
|
||||
});
|
||||
if (confirmed) {
|
||||
const result = await comms.pickerZHome();
|
||||
if (!result.success) {
|
||||
showAlert({ type: 'error', title: 'Home Failed', message: result.message });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleZZero = async () => {
|
||||
const confirmed = await showConfirm({
|
||||
title: 'Z-Axis Zero',
|
||||
message: 'Do you want to move picker Z-axis to coordinate:0?'
|
||||
});
|
||||
if (confirmed) {
|
||||
const result = await comms.pickerZZero();
|
||||
if (!result.success) {
|
||||
showAlert({ type: 'error', title: 'Move Failed', message: result.message });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// === Print Test ===
|
||||
const handlePrintL = async () => {
|
||||
const result = await comms.pickerTestPrint('left');
|
||||
if (!result.success) {
|
||||
showAlert({ type: 'error', title: 'Print Failed', message: result.message });
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrintR = async () => {
|
||||
const result = await comms.pickerTestPrint('right');
|
||||
if (!result.success) {
|
||||
showAlert({ type: 'error', title: 'Print Failed', message: result.message });
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[100] flex items-center justify-center">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 bg-black/80 backdrop-blur-sm"
|
||||
onClick={handleClose}
|
||||
/>
|
||||
|
||||
{/* Dialog */}
|
||||
<div className="relative bg-black/95 backdrop-blur-md border-2 border-neon-blue rounded-lg shadow-2xl w-full max-w-4xl mx-4 animate-fadeIn">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-white/10">
|
||||
<div className="flex items-center gap-3">
|
||||
<Move className="w-6 h-6 text-neon-blue" />
|
||||
<h2 className="text-xl font-tech font-bold text-white uppercase tracking-wider">
|
||||
Picker(X) Movement and Management
|
||||
</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="text-slate-400 hover:text-white transition-colors"
|
||||
>
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-4">
|
||||
{/* Row 1: Jog Controls */}
|
||||
<div className="grid grid-cols-5 gap-2 mb-2">
|
||||
{/* Z Up */}
|
||||
<button
|
||||
onMouseDown={() => handleJogStart('up')}
|
||||
onMouseUp={handleJogStop}
|
||||
onMouseLeave={handleJogStop}
|
||||
disabled={!status.zEnabled}
|
||||
className="h-24 flex items-center justify-center bg-slate-800/50 hover:bg-slate-700/50 disabled:opacity-50 disabled:cursor-not-allowed border border-white/10 rounded-lg text-purple-400 transition-colors"
|
||||
>
|
||||
<ChevronUp className="w-16 h-16" strokeWidth={3} />
|
||||
</button>
|
||||
|
||||
{/* X Left Jog */}
|
||||
<button
|
||||
onMouseDown={() => handleJogStart('left')}
|
||||
onMouseUp={handleJogStop}
|
||||
onMouseLeave={handleJogStop}
|
||||
disabled={!status.xEnabled}
|
||||
className="h-24 flex items-center justify-center bg-slate-800/50 hover:bg-slate-700/50 disabled:opacity-50 disabled:cursor-not-allowed border border-white/10 rounded-lg text-blue-900 transition-colors"
|
||||
>
|
||||
<ChevronLeft className="w-16 h-16" strokeWidth={3} />
|
||||
</button>
|
||||
|
||||
{/* Stop */}
|
||||
<button
|
||||
onClick={handleStop}
|
||||
className={`h-24 flex items-center justify-center border border-white/10 rounded-lg transition-colors ${
|
||||
status.pickerSafe
|
||||
? 'bg-green-500/30 hover:bg-green-500/40 text-green-400'
|
||||
: 'bg-red-500/30 hover:bg-red-500/40 text-red-400'
|
||||
}`}
|
||||
>
|
||||
<Square className="w-16 h-16" fill="currentColor" />
|
||||
</button>
|
||||
|
||||
{/* X Right Jog */}
|
||||
<button
|
||||
onMouseDown={() => handleJogStart('right')}
|
||||
onMouseUp={handleJogStop}
|
||||
onMouseLeave={handleJogStop}
|
||||
disabled={!status.xEnabled}
|
||||
className="h-24 flex items-center justify-center bg-slate-800/50 hover:bg-slate-700/50 disabled:opacity-50 disabled:cursor-not-allowed border border-white/10 rounded-lg text-blue-900 transition-colors"
|
||||
>
|
||||
<ChevronRight className="w-16 h-16" strokeWidth={3} />
|
||||
</button>
|
||||
|
||||
{/* Z Down */}
|
||||
<button
|
||||
onMouseDown={() => handleJogStart('down')}
|
||||
onMouseUp={handleJogStop}
|
||||
onMouseLeave={handleJogStop}
|
||||
disabled={!status.zEnabled}
|
||||
className="h-24 flex items-center justify-center bg-slate-800/50 hover:bg-slate-700/50 disabled:opacity-50 disabled:cursor-not-allowed border border-white/10 rounded-lg text-purple-400 transition-colors"
|
||||
>
|
||||
<ChevronDown className="w-16 h-16" strokeWidth={3} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Row 2: Position Buttons */}
|
||||
<div className="grid grid-cols-5 gap-2 mb-2">
|
||||
<button
|
||||
onClick={handleMoveLeft}
|
||||
disabled={!status.managementEnabled}
|
||||
className="h-24 flex items-center justify-center bg-slate-800/50 hover:bg-slate-700/50 disabled:opacity-50 disabled:cursor-not-allowed border border-white/10 rounded-lg text-white font-tech text-2xl font-bold transition-colors"
|
||||
>
|
||||
Left
|
||||
</button>
|
||||
<button
|
||||
onClick={handleMoveLeftWait}
|
||||
disabled={!status.managementEnabled}
|
||||
className="h-24 flex items-center justify-center bg-slate-800/50 hover:bg-slate-700/50 disabled:opacity-50 disabled:cursor-not-allowed border border-white/10 rounded-lg text-white font-tech text-2xl font-bold transition-colors"
|
||||
>
|
||||
Wait
|
||||
</button>
|
||||
<button
|
||||
onClick={handleMoveCenter}
|
||||
disabled={!status.managementEnabled}
|
||||
className="h-24 flex items-center justify-center bg-slate-800/50 hover:bg-slate-700/50 disabled:opacity-50 disabled:cursor-not-allowed border border-white/10 rounded-lg text-green-400 font-tech text-2xl font-bold transition-colors"
|
||||
>
|
||||
Center
|
||||
</button>
|
||||
<button
|
||||
onClick={handleMoveRightWait}
|
||||
disabled={!status.managementEnabled}
|
||||
className="h-24 flex items-center justify-center bg-slate-800/50 hover:bg-slate-700/50 disabled:opacity-50 disabled:cursor-not-allowed border border-white/10 rounded-lg text-white font-tech text-2xl font-bold transition-colors"
|
||||
>
|
||||
Wait
|
||||
</button>
|
||||
<button
|
||||
onClick={handleMoveRight}
|
||||
disabled={!status.managementEnabled}
|
||||
className="h-24 flex items-center justify-center bg-slate-800/50 hover:bg-slate-700/50 disabled:opacity-50 disabled:cursor-not-allowed border border-white/10 rounded-lg text-white font-tech text-2xl font-bold transition-colors"
|
||||
>
|
||||
Right
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Row 3: Vision Cancel & Management Position */}
|
||||
<div className="grid grid-cols-5 gap-2 mb-2">
|
||||
<button
|
||||
onClick={handleCancelVisionL}
|
||||
className="h-24 flex flex-col items-center justify-center bg-amber-900/30 hover:bg-amber-900/50 border border-amber-500/50 rounded-lg text-amber-300 font-tech font-bold transition-colors"
|
||||
>
|
||||
<XCircle className="w-6 h-6 mb-1" />
|
||||
<span className="text-sm">Vision Validation</span>
|
||||
<span className="text-sm">Cancel(L)</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleManagePosL}
|
||||
disabled={!status.managementEnabled}
|
||||
className="h-24 flex flex-col items-center justify-center bg-purple-900/30 hover:bg-purple-900/50 disabled:opacity-50 disabled:cursor-not-allowed border border-purple-500/50 rounded-lg text-purple-300 font-tech font-bold transition-colors"
|
||||
>
|
||||
<Settings className="w-6 h-6 mb-1" />
|
||||
<span className="text-sm">Print Management</span>
|
||||
<span className="text-sm">Position(L)</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleManagePosReturn}
|
||||
disabled={!status.managementEnabled}
|
||||
className="h-24 flex flex-col items-center justify-center bg-purple-900/30 hover:bg-purple-900/50 disabled:opacity-50 disabled:cursor-not-allowed border border-purple-500/50 rounded-lg text-purple-300 font-tech font-bold transition-colors"
|
||||
>
|
||||
<Home className="w-6 h-6 mb-1" />
|
||||
<span className="text-sm">Management Position</span>
|
||||
<span className="text-sm">Return</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleManagePosR}
|
||||
disabled={!status.managementEnabled}
|
||||
className="h-24 flex flex-col items-center justify-center bg-purple-900/30 hover:bg-purple-900/50 disabled:opacity-50 disabled:cursor-not-allowed border border-purple-500/50 rounded-lg text-purple-300 font-tech font-bold transition-colors"
|
||||
>
|
||||
<Settings className="w-6 h-6 mb-1" />
|
||||
<span className="text-sm">Print Management</span>
|
||||
<span className="text-sm">Position(R)</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCancelVisionR}
|
||||
className="h-24 flex flex-col items-center justify-center bg-amber-900/30 hover:bg-amber-900/50 border border-amber-500/50 rounded-lg text-amber-300 font-tech font-bold transition-colors"
|
||||
>
|
||||
<XCircle className="w-6 h-6 mb-1" />
|
||||
<span className="text-sm">Vision Validation</span>
|
||||
<span className="text-sm">Cancel(R)</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Row 4: Z-Home, Print, Z-Zero */}
|
||||
<div className="grid grid-cols-5 gap-2">
|
||||
<button
|
||||
onClick={handleZHome}
|
||||
className="h-24 flex flex-col items-center justify-center bg-slate-800/50 hover:bg-slate-700/50 border border-white/10 rounded-lg text-white font-tech text-lg font-bold transition-colors"
|
||||
>
|
||||
<Home className="w-6 h-6 mb-1" />
|
||||
<span>Z-HOME</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={handlePrintL}
|
||||
className="h-24 flex flex-col items-center justify-center bg-slate-800/50 hover:bg-slate-700/50 border border-white/10 rounded-lg text-white font-tech text-lg font-bold transition-colors"
|
||||
>
|
||||
<Printer className="w-6 h-6 mb-1" />
|
||||
<span>PRINT(L)</span>
|
||||
</button>
|
||||
<button
|
||||
disabled
|
||||
className="h-24 flex items-center justify-center bg-slate-800/30 border border-white/10 rounded-lg text-slate-500 font-tech text-lg font-bold cursor-not-allowed"
|
||||
>
|
||||
--
|
||||
</button>
|
||||
<button
|
||||
onClick={handlePrintR}
|
||||
className="h-24 flex flex-col items-center justify-center bg-slate-800/50 hover:bg-slate-700/50 border border-white/10 rounded-lg text-white font-tech text-lg font-bold transition-colors"
|
||||
>
|
||||
<Printer className="w-6 h-6 mb-1" />
|
||||
<span>PRINT(R)</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleZZero}
|
||||
className="h-24 flex flex-col items-center justify-center bg-slate-800/50 hover:bg-slate-700/50 border border-white/10 rounded-lg text-white font-tech text-lg font-bold transition-colors"
|
||||
>
|
||||
<span className="text-2xl mb-1">0</span>
|
||||
<span>Z-ZERO</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex justify-end p-4 border-t border-white/10">
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="px-6 py-2 bg-slate-700 hover:bg-slate-600 border border-white/20 text-white font-tech font-bold rounded transition-colors"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,13 +1,15 @@
|
||||
import React from 'react';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { LucideIcon } from 'lucide-react';
|
||||
|
||||
interface PanelHeaderProps {
|
||||
title: string;
|
||||
icon: LucideIcon;
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const PanelHeader: React.FC<PanelHeaderProps> = ({ title, icon: Icon }) => (
|
||||
<div className="flex items-center gap-3 mb-6 border-b border-white/10 pb-2">
|
||||
export const PanelHeader: React.FC<PanelHeaderProps> = ({ title, icon: Icon, className, children }) => (
|
||||
<div className={`flex items-center gap-3 mb-6 border-b border-white/10 pb-2 ${className || ''}`}>
|
||||
<div className="text-neon-blue animate-pulse">
|
||||
<Icon className="w-5 h-5" />
|
||||
</div>
|
||||
@@ -15,5 +17,6 @@ export const PanelHeader: React.FC<PanelHeaderProps> = ({ title, icon: Icon }) =
|
||||
{title}
|
||||
</h2>
|
||||
<div className="flex-1 h-px bg-gradient-to-r from-neon-blue/50 to-transparent"></div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,24 +1,68 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { RobotTarget } from '../../types';
|
||||
import { comms } from '../../communication';
|
||||
|
||||
// HW 상태 타입 (윈폼 HWState와 동일)
|
||||
// status: 0=SET(미설정/회색), 1=ON(연결/녹색), 2=TRIG(트리거/노란색), 3=OFF(연결안됨/빨간색)
|
||||
interface HWItem {
|
||||
name: string;
|
||||
title: string;
|
||||
status: number;
|
||||
}
|
||||
|
||||
interface FooterProps {
|
||||
isHostConnected: boolean;
|
||||
robotTarget: RobotTarget;
|
||||
}
|
||||
|
||||
// 상태에 따른 LED 색상 반환
|
||||
const getStatusColor = (status: number): { bg: string; shadow: string; text: string } => {
|
||||
switch (status) {
|
||||
case 1: // ON - 녹색
|
||||
return { bg: 'bg-neon-green', shadow: 'shadow-[0_0_5px_#0aff00]', text: 'text-green-400' };
|
||||
case 2: // TRIG - 노란색
|
||||
return { bg: 'bg-yellow-400', shadow: 'shadow-[0_0_5px_#facc15]', text: 'text-yellow-400' };
|
||||
case 3: // OFF - 빨간색
|
||||
return { bg: 'bg-red-500', shadow: 'shadow-[0_0_5px_#ef4444]', text: 'text-red-400' };
|
||||
default: // SET - 회색 (미설정)
|
||||
return { bg: 'bg-gray-500', shadow: '', text: 'text-gray-400' };
|
||||
}
|
||||
};
|
||||
|
||||
export const Footer: React.FC<FooterProps> = ({ isHostConnected, robotTarget }) => {
|
||||
const [hwStatus, setHwStatus] = useState<HWItem[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
// HW_STATUS_UPDATE 이벤트 구독
|
||||
const unsubscribe = comms.subscribe((msg: any) => {
|
||||
if (msg?.type === 'HW_STATUS_UPDATE' && msg.data) {
|
||||
setHwStatus(msg.data);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<footer className="absolute bottom-0 left-0 right-0 h-10 bg-black/80 border-t border-neon-blue/30 flex items-center px-6 justify-between z-40 backdrop-blur text-xs font-mono text-slate-400">
|
||||
<div className="flex gap-6">
|
||||
{['PLC', 'MOTION', 'VISION', 'LIGHT'].map(hw => (
|
||||
<div key={hw} className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-neon-green rounded-full shadow-[0_0_5px_#0aff00]"></div>
|
||||
<span className="font-bold text-slate-300">{hw}</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex gap-4">
|
||||
{/* H/W 상태 표시 (윈폼 HWState와 동일) */}
|
||||
{hwStatus.map((hw) => {
|
||||
const colors = getStatusColor(hw.status);
|
||||
return (
|
||||
<div key={hw.name} className="flex items-center gap-1.5" title={`${hw.name}: ${hw.title}`}>
|
||||
<div className={`w-2 h-2 rounded-full transition-all ${colors.bg} ${colors.shadow}`}></div>
|
||||
<span className={`font-bold ${colors.text}`}>{hw.name}</span>
|
||||
<span className="text-[10px] text-slate-500">{hw.title}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{/* HOST 연결 상태 */}
|
||||
<div className="flex items-center gap-1.5 ml-2 pl-2 border-l border-slate-700">
|
||||
<div className={`w-2 h-2 rounded-full transition-all ${isHostConnected ? 'bg-neon-green shadow-[0_0_5px_#0aff00]' : 'bg-red-500 shadow-[0_0_5px_#ff0000] animate-pulse'}`}></div>
|
||||
<span className={`font-bold ${isHostConnected ? 'text-slate-300' : 'text-red-400'}`}>HOST</span>
|
||||
<span className={`font-bold ${isHostConnected ? 'text-green-400' : 'text-red-400'}`}>HOST</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-8 text-neon-blue">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { Activity, Settings, Move, Camera, Layers, Cpu, Target, Lightbulb, Printer, XCircle, Package, BookOpen } from 'lucide-react';
|
||||
import { Activity, Settings, Move, Camera, Layers, Cpu, Target, Lightbulb, Printer, XCircle, Package, BookOpen, History, Clock } from 'lucide-react';
|
||||
import { VisionMenu } from '../VisionMenu';
|
||||
import { FunctionMenu } from '../FunctionMenu';
|
||||
import { ManualPrintDialog, PrintData } from '../ManualPrintDialog';
|
||||
@@ -127,6 +127,20 @@ export const Header: React.FC<HeaderProps> = ({ currentTime, onTabChange, active
|
||||
<XCircle className="w-5 h-5" />
|
||||
<span className="leading-tight">CANCEL</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate('/history');
|
||||
onTabChange(null);
|
||||
}}
|
||||
className={`flex flex-col items-center justify-center gap-1 px-3 py-2 rounded-xl font-tech font-bold text-[10px] transition-all border border-transparent min-w-[70px] ${location.pathname === '/history'
|
||||
? 'bg-neon-blue/10 text-neon-blue border-neon-blue shadow-glow-blue'
|
||||
: 'text-slate-400 hover:text-purple-400 hover:bg-white/5'
|
||||
}`}
|
||||
title="View Work History"
|
||||
>
|
||||
<Clock className="w-5 h-5" />
|
||||
<span className="leading-tight">HISTORY</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Main Navigation */}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { createContext, useContext, useState, ReactNode } from 'react';
|
||||
import React, { createContext, useContext, useState, ReactNode, useCallback, useRef } from 'react';
|
||||
|
||||
interface AlertConfig {
|
||||
type: 'success' | 'error' | 'warning' | 'info';
|
||||
@@ -6,8 +6,14 @@ interface AlertConfig {
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface ConfirmConfig {
|
||||
title: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface AlertContextType {
|
||||
showAlert: (config: AlertConfig) => void;
|
||||
showConfirm: (config: ConfirmConfig) => Promise<boolean>;
|
||||
}
|
||||
|
||||
const AlertContext = createContext<AlertContextType | undefined>(undefined);
|
||||
@@ -26,6 +32,8 @@ interface AlertProviderProps {
|
||||
|
||||
export const AlertProvider: React.FC<AlertProviderProps> = ({ children }) => {
|
||||
const [alertConfig, setAlertConfig] = useState<(AlertConfig & { isOpen: boolean }) | null>(null);
|
||||
const [confirmConfig, setConfirmConfig] = useState<(ConfirmConfig & { isOpen: boolean }) | null>(null);
|
||||
const confirmResolveRef = useRef<((value: boolean) => void) | null>(null);
|
||||
|
||||
const showAlert = (config: AlertConfig) => {
|
||||
setAlertConfig({ ...config, isOpen: true });
|
||||
@@ -35,9 +43,25 @@ export const AlertProvider: React.FC<AlertProviderProps> = ({ children }) => {
|
||||
setAlertConfig(null);
|
||||
};
|
||||
|
||||
const showConfirm = useCallback((config: ConfirmConfig): Promise<boolean> => {
|
||||
return new Promise((resolve) => {
|
||||
confirmResolveRef.current = resolve;
|
||||
setConfirmConfig({ ...config, isOpen: true });
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleConfirm = (result: boolean) => {
|
||||
setConfirmConfig(null);
|
||||
if (confirmResolveRef.current) {
|
||||
confirmResolveRef.current(result);
|
||||
confirmResolveRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AlertContext.Provider value={{ showAlert }}>
|
||||
<AlertContext.Provider value={{ showAlert, showConfirm }}>
|
||||
{children}
|
||||
{/* Alert Dialog */}
|
||||
{alertConfig && (
|
||||
<div className="fixed inset-0 z-[100] flex items-center justify-center">
|
||||
{/* Backdrop */}
|
||||
@@ -77,6 +101,53 @@ export const AlertProvider: React.FC<AlertProviderProps> = ({ children }) => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Confirm Dialog */}
|
||||
{confirmConfig && (
|
||||
<div className="fixed inset-0 z-[100] flex items-center justify-center">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 bg-black/80 backdrop-blur-sm"
|
||||
onClick={() => handleConfirm(false)}
|
||||
/>
|
||||
|
||||
{/* Dialog */}
|
||||
<div className="relative bg-black/95 backdrop-blur-md border-2 border-amber-500 rounded-lg shadow-2xl max-w-md w-full mx-4 animate-fadeIn">
|
||||
{/* Icon and Title */}
|
||||
<div className="flex items-center gap-4 p-6 border-b border-white/10">
|
||||
<svg className="w-12 h-12 text-amber-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<h2 className="text-xl font-tech font-bold text-white uppercase tracking-wider">
|
||||
{confirmConfig.title}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* Message */}
|
||||
<div className="p-6 max-w-full overflow-hidden">
|
||||
<p className="text-slate-300 font-mono text-sm whitespace-pre-wrap leading-relaxed break-words max-w-full">
|
||||
{confirmConfig.message}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Buttons */}
|
||||
<div className="flex justify-end gap-3 p-6 border-t border-white/10">
|
||||
<button
|
||||
onClick={() => handleConfirm(false)}
|
||||
className="px-6 py-2 bg-slate-700 hover:bg-slate-600 border border-white/20 text-white font-tech font-bold rounded transition-colors"
|
||||
>
|
||||
No
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleConfirm(true)}
|
||||
className="px-6 py-2 bg-amber-500/20 hover:bg-amber-500/30 border border-amber-500 text-amber-400 font-tech font-bold rounded transition-colors"
|
||||
>
|
||||
Yes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</AlertContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
320
FrontEnd/pages/HistoryPage.tsx
Normal file
320
FrontEnd/pages/HistoryPage.tsx
Normal file
@@ -0,0 +1,320 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { ArrowLeft, Search, Download, ChevronLeft, ChevronRight, Filter, X } from 'lucide-react';
|
||||
import { comms } from '../communication';
|
||||
|
||||
interface HistoryRow {
|
||||
idx: number;
|
||||
jtype: string;
|
||||
rid: string;
|
||||
sid: string;
|
||||
qty: number;
|
||||
vname: string;
|
||||
vlot: string;
|
||||
loc: string;
|
||||
qr: string;
|
||||
stime: string;
|
||||
etime: string;
|
||||
prnattach: boolean;
|
||||
prnvalid: boolean;
|
||||
rid0: string;
|
||||
sid0: string;
|
||||
qtymax: number;
|
||||
}
|
||||
|
||||
export const HistoryPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [data, setData] = useState<HistoryRow[]>([]);
|
||||
const [filteredData, setFilteredData] = useState<HistoryRow[]>([]);
|
||||
const [mcName, setMcName] = useState('');
|
||||
|
||||
// Date range state
|
||||
const today = new Date();
|
||||
const [startDate, setStartDate] = useState(today.toISOString().split('T')[0]);
|
||||
const [endDate, setEndDate] = useState(today.toISOString().split('T')[0]);
|
||||
|
||||
// Search state
|
||||
const [dbSearch, setDbSearch] = useState('');
|
||||
const [localFilter, setLocalFilter] = useState('');
|
||||
|
||||
// Load data on mount
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
// Apply local filter when it changes
|
||||
useEffect(() => {
|
||||
if (!localFilter.trim()) {
|
||||
setFilteredData(data);
|
||||
} else {
|
||||
const keyword = localFilter.toLowerCase();
|
||||
setFilteredData(data.filter(row =>
|
||||
row.rid0?.toLowerCase().includes(keyword) ||
|
||||
row.sid0?.toLowerCase().includes(keyword) ||
|
||||
row.qr?.toLowerCase().includes(keyword) ||
|
||||
row.vlot?.toLowerCase().includes(keyword) ||
|
||||
row.vname?.toLowerCase().includes(keyword) ||
|
||||
row.rid?.toLowerCase().includes(keyword) ||
|
||||
row.sid?.toLowerCase().includes(keyword)
|
||||
));
|
||||
}
|
||||
}, [localFilter, data]);
|
||||
|
||||
const fetchData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const result = await comms.getHistoryData(startDate, endDate, dbSearch);
|
||||
if (result.success) {
|
||||
setData(result.data || []);
|
||||
setFilteredData(result.data || []);
|
||||
if (result.mcName) {
|
||||
setMcName(result.mcName);
|
||||
}
|
||||
} else {
|
||||
console.error('Failed to fetch history:', result.message);
|
||||
setData([]);
|
||||
setFilteredData([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching history:', error);
|
||||
setData([]);
|
||||
setFilteredData([]);
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
// Validate dates
|
||||
if (new Date(endDate) < new Date(startDate)) {
|
||||
alert('End date is earlier than start date');
|
||||
return;
|
||||
}
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const handlePrevDay = () => {
|
||||
const sd = new Date(startDate);
|
||||
sd.setDate(sd.getDate() - 1);
|
||||
const newDate = sd.toISOString().split('T')[0];
|
||||
setStartDate(newDate);
|
||||
setEndDate(newDate);
|
||||
};
|
||||
|
||||
const handleNextDay = () => {
|
||||
const ed = new Date(endDate);
|
||||
ed.setDate(ed.getDate() + 1);
|
||||
const newDate = ed.toISOString().split('T')[0];
|
||||
setStartDate(newDate);
|
||||
setEndDate(newDate);
|
||||
};
|
||||
|
||||
const handleExport = () => {
|
||||
if (filteredData.length === 0) {
|
||||
alert('No data to export');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create CSV content
|
||||
const headers = ['IDX', 'JTYPE', 'RID', 'SID', 'QTY', 'QTY_MAX', 'VENDOR', 'V.LOT', 'LOC', 'QR', 'START', 'END', 'ATTACH', 'VALID', 'RID0', 'SID0'];
|
||||
const csvContent = [
|
||||
headers.join(','),
|
||||
...filteredData.map(row => [
|
||||
row.idx,
|
||||
`"${row.jtype || ''}"`,
|
||||
`"${row.rid || ''}"`,
|
||||
`"${row.sid || ''}"`,
|
||||
row.qty || 0,
|
||||
row.qtymax || 0,
|
||||
`"${row.vname || ''}"`,
|
||||
`"${row.vlot || ''}"`,
|
||||
`"${row.loc || ''}"`,
|
||||
`"${row.qr || ''}"`,
|
||||
`"${row.stime || ''}"`,
|
||||
`"${row.etime || ''}"`,
|
||||
row.prnattach ? 'Y' : 'N',
|
||||
row.prnvalid ? 'Y' : 'N',
|
||||
`"${row.rid0 || ''}"`,
|
||||
`"${row.sid0 || ''}"`,
|
||||
].join(','))
|
||||
].join('\n');
|
||||
|
||||
// Download file
|
||||
const blob = new Blob(['\uFEFF' + csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = `export_${startDate.replace(/-/g, '')}~${endDate.replace(/-/g, '')}.csv`;
|
||||
link.click();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white p-4">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={() => navigate('/')}
|
||||
className="p-2 rounded-lg bg-gray-800 hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</button>
|
||||
<h1 className="text-2xl font-tech font-bold text-neon-blue">
|
||||
WORK HISTORY {mcName && <span className="text-gray-400 text-lg">({mcName})</span>}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="text-sm text-gray-400">
|
||||
Total: <span className="text-white font-bold">{filteredData.length}</span> records
|
||||
{localFilter && <span className="text-yellow-400 ml-2">(filtered from {data.length})</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Controls */}
|
||||
<div className="flex flex-wrap gap-4 mb-4 p-4 bg-gray-800/50 rounded-xl border border-gray-700">
|
||||
{/* Date Range */}
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handlePrevDay}
|
||||
className="p-2 rounded-lg bg-gray-700 hover:bg-gray-600 transition-colors"
|
||||
title="Previous Day"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</button>
|
||||
<input
|
||||
type="date"
|
||||
value={startDate}
|
||||
onChange={(e) => setStartDate(e.target.value)}
|
||||
className="px-3 py-2 rounded-lg bg-gray-700 border border-gray-600 text-white text-sm focus:outline-none focus:border-neon-blue"
|
||||
/>
|
||||
<span className="text-gray-500">~</span>
|
||||
<input
|
||||
type="date"
|
||||
value={endDate}
|
||||
onChange={(e) => setEndDate(e.target.value)}
|
||||
className="px-3 py-2 rounded-lg bg-gray-700 border border-gray-600 text-white text-sm focus:outline-none focus:border-neon-blue"
|
||||
/>
|
||||
<button
|
||||
onClick={handleNextDay}
|
||||
className="p-2 rounded-lg bg-gray-700 hover:bg-gray-600 transition-colors"
|
||||
title="Next Day"
|
||||
>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* DB Search */}
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={dbSearch}
|
||||
onChange={(e) => setDbSearch(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
placeholder="Search in DB..."
|
||||
className="px-3 py-2 rounded-lg bg-gray-700 border border-gray-600 text-white text-sm focus:outline-none focus:border-neon-blue w-48"
|
||||
/>
|
||||
<button
|
||||
onClick={handleSearch}
|
||||
disabled={isLoading}
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-lg bg-neon-blue/20 text-neon-blue border border-neon-blue hover:bg-neon-blue/30 transition-colors disabled:opacity-50"
|
||||
>
|
||||
<Search className="w-4 h-4" />
|
||||
<span>Search</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Local Filter */}
|
||||
<div className="flex items-center gap-2 ml-auto">
|
||||
<Filter className="w-4 h-4 text-gray-500" />
|
||||
<input
|
||||
type="text"
|
||||
value={localFilter}
|
||||
onChange={(e) => setLocalFilter(e.target.value)}
|
||||
placeholder="Filter results..."
|
||||
className={`px-3 py-2 rounded-lg border text-white text-sm focus:outline-none focus:border-neon-blue w-48 ${localFilter ? 'bg-lime-900/30 border-lime-500' : 'bg-gray-700 border-gray-600'
|
||||
}`}
|
||||
/>
|
||||
{localFilter && (
|
||||
<button
|
||||
onClick={() => setLocalFilter('')}
|
||||
className="p-1 rounded hover:bg-gray-700"
|
||||
>
|
||||
<X className="w-4 h-4 text-gray-400" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Export */}
|
||||
<button
|
||||
onClick={handleExport}
|
||||
disabled={filteredData.length === 0}
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-lg bg-green-600/20 text-green-400 border border-green-600 hover:bg-green-600/30 transition-colors disabled:opacity-50"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
<span>Export CSV</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Data Table */}
|
||||
<div className="bg-gray-800/50 rounded-xl border border-gray-700 overflow-hidden">
|
||||
<div className="overflow-x-auto max-h-[calc(100vh-280px)]">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="sticky top-0 bg-gray-800 border-b border-gray-700">
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-left text-gray-400 font-medium">IDX</th>
|
||||
<th className="px-3 py-2 text-left text-gray-400 font-medium">TYPE</th>
|
||||
<th className="px-3 py-2 text-left text-gray-400 font-medium">RID</th>
|
||||
<th className="px-3 py-2 text-left text-gray-400 font-medium">SID</th>
|
||||
<th className="px-3 py-2 text-left text-gray-400 font-medium">QTY</th>
|
||||
<th className="px-3 py-2 text-left text-gray-400 font-medium">VENDOR</th>
|
||||
<th className="px-3 py-2 text-left text-gray-400 font-medium">V.LOT</th>
|
||||
<th className="px-3 py-2 text-left text-gray-400 font-medium">LOC</th>
|
||||
<th className="px-3 py-2 text-left text-gray-400 font-medium">START</th>
|
||||
<th className="px-3 py-2 text-left text-gray-400 font-medium">END</th>
|
||||
<th className="px-3 py-2 text-left text-gray-400 font-medium">ATTACH</th>
|
||||
<th className="px-3 py-2 text-left text-gray-400 font-medium">VALID</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{isLoading ? (
|
||||
<tr>
|
||||
<td colSpan={12} className="px-3 py-8 text-center text-gray-500">
|
||||
Loading...
|
||||
</td>
|
||||
</tr>
|
||||
) : filteredData.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={12} className="px-3 py-8 text-center text-gray-500">
|
||||
No data found
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
filteredData.map((row, index) => (
|
||||
<tr
|
||||
key={row.idx || index}
|
||||
className="border-b border-gray-700/50 hover:bg-gray-700/30 transition-colors"
|
||||
>
|
||||
<td className="px-3 py-2 text-gray-500">{row.idx}</td>
|
||||
<td className="px-3 py-2 text-xs">{row.jtype}</td>
|
||||
<td className="px-3 py-2 font-mono text-xs">{row.rid}</td>
|
||||
<td className="px-3 py-2 font-mono text-xs">{row.sid}</td>
|
||||
<td className="px-3 py-2 text-right">{row.qty}/{row.qtymax}</td>
|
||||
<td className="px-3 py-2">{row.vname}</td>
|
||||
<td className="px-3 py-2 font-mono text-xs">{row.vlot}</td>
|
||||
<td className="px-3 py-2 text-center">{row.loc}</td>
|
||||
<td className="px-3 py-2 text-xs text-gray-400">{row.stime}</td>
|
||||
<td className="px-3 py-2 text-xs text-gray-400">{row.etime}</td>
|
||||
<td className={`px-3 py-2 text-center font-bold ${row.prnattach ? 'text-green-400' : 'text-red-400'}`}>
|
||||
{row.prnattach ? 'Y' : 'N'}
|
||||
</td>
|
||||
<td className={`px-3 py-2 text-center font-bold ${row.prnvalid ? 'text-green-400' : 'text-red-400'}`}>
|
||||
{row.prnvalid ? 'Y' : 'N'}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -51,7 +51,8 @@ export const IOMonitorPage: React.FC<IOMonitorPageProps> = ({ onToggle }) => {
|
||||
|
||||
// Subscribe to real-time IO updates
|
||||
const unsubscribe = comms.subscribe((msg: any) => {
|
||||
if (msg?.type === 'STATUS_UPDATE' && msg.ioState) {
|
||||
// STATUS_UPDATE - 주기적인 상태 업데이트 (변경된 IO만 포함)
|
||||
if (msg?.type === 'STATUS_UPDATE' && msg.ioState && msg.ioState.length > 0) {
|
||||
setIoPoints(prev => {
|
||||
const newIO = [...prev];
|
||||
msg.ioState.forEach((update: { id: number, type: string, state: boolean }) => {
|
||||
@@ -61,6 +62,39 @@ export const IOMonitorPage: React.FC<IOMonitorPageProps> = ({ onToggle }) => {
|
||||
return newIO;
|
||||
});
|
||||
}
|
||||
|
||||
// IO_CHANGED - 개별 IO 값 변경 이벤트 (실시간)
|
||||
if (msg?.type === 'IO_CHANGED' && msg.data) {
|
||||
const { id, ioType, state } = msg.data;
|
||||
setIoPoints(prev => {
|
||||
const newIO = [...prev];
|
||||
const idx = newIO.findIndex(p => p.id === id && p.type === ioType);
|
||||
if (idx >= 0) {
|
||||
newIO[idx] = { ...newIO[idx], state: state };
|
||||
}
|
||||
return newIO;
|
||||
});
|
||||
}
|
||||
|
||||
// INTERLOCK_CHANGED - 인터락 값 변경 이벤트 (실시간)
|
||||
if (msg?.type === 'INTERLOCK_CHANGED' && msg.data) {
|
||||
const { axisIndex, lockIndex, state, hexValue } = msg.data;
|
||||
setInterlocks(prev => {
|
||||
const newInterlocks = [...prev];
|
||||
const axisIdx = newInterlocks.findIndex(a => a.axisIndex === axisIndex);
|
||||
if (axisIdx >= 0) {
|
||||
const lockIdx = newInterlocks[axisIdx].locks.findIndex(l => l.id === lockIndex);
|
||||
if (lockIdx >= 0) {
|
||||
newInterlocks[axisIdx].locks[lockIdx] = {
|
||||
...newInterlocks[axisIdx].locks[lockIdx],
|
||||
state: state
|
||||
};
|
||||
newInterlocks[axisIdx].hexValue = hexValue;
|
||||
}
|
||||
}
|
||||
return newInterlocks;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
@@ -72,6 +106,19 @@ export const IOMonitorPage: React.FC<IOMonitorPageProps> = ({ onToggle }) => {
|
||||
? []
|
||||
: ioPoints.filter(p => p.type === (activeIOTab === 'in' ? 'input' : 'output'));
|
||||
|
||||
// 인터락 토글 핸들러 (DIOMonitor.cs의 gvILXF_ItemClick과 동일)
|
||||
const handleInterlockToggle = async (axisIndex: number, lockIndex: number) => {
|
||||
try {
|
||||
const result = await comms.toggleInterlock(axisIndex, lockIndex);
|
||||
if (!result.success) {
|
||||
console.error('[IOMonitor] Interlock toggle failed:', result.message);
|
||||
}
|
||||
// 성공 시 INTERLOCK_CHANGED 이벤트로 UI가 자동 업데이트됨
|
||||
} catch (error) {
|
||||
console.error('[IOMonitor] Interlock toggle error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="relative w-full h-full px-6 pt-6 pb-20">
|
||||
<div className="glass-holo p-6 h-full flex flex-col gap-4">
|
||||
@@ -130,11 +177,13 @@ export const IOMonitorPage: React.FC<IOMonitorPageProps> = ({ onToggle }) => {
|
||||
{axis.locks.map(lock => (
|
||||
<div
|
||||
key={lock.id}
|
||||
onClick={() => handleInterlockToggle(axis.axisIndex, lock.id)}
|
||||
className={`
|
||||
flex items-center gap-2 px-3 py-2 transition-all border rounded
|
||||
flex items-center gap-2 px-3 py-2 transition-all border rounded cursor-pointer
|
||||
hover:translate-x-0.5 hover:brightness-110
|
||||
${lock.state
|
||||
? 'bg-red-500/20 border-red-500 text-red-400 shadow-[0_0_10px_rgba(239,68,68,0.3)]'
|
||||
: 'bg-slate-800/40 border-slate-700 text-slate-500'}
|
||||
: 'bg-slate-800/40 border-slate-700 text-slate-500 hover:border-slate-600'}
|
||||
`}
|
||||
>
|
||||
<div className={`w-2 h-2 rounded-full shrink-0 ${lock.state ? 'bg-red-500 shadow-[0_0_6px_#ef4444]' : 'bg-slate-700'}`}></div>
|
||||
|
||||
@@ -99,6 +99,56 @@ declare global {
|
||||
InitializeDevice(): Promise<string>;
|
||||
GetInitializeStatus(): Promise<string>;
|
||||
GetProcessedData(): Promise<string>;
|
||||
// Vision control methods
|
||||
CameraConnect(): Promise<string>;
|
||||
CameraDisconnect(): Promise<string>;
|
||||
CameraGetImage(): Promise<string>;
|
||||
CameraLiveView(): Promise<string>;
|
||||
CameraReadTest(): Promise<string>;
|
||||
KeyenceTriggerOn(): Promise<string>;
|
||||
KeyenceTriggerOff(): Promise<string>;
|
||||
KeyenceGetImage(): Promise<string>;
|
||||
KeyenceSaveImage(): Promise<string>;
|
||||
// Light, Manual Print, Cancel Job
|
||||
ToggleLight(): Promise<string>;
|
||||
ExecuteManualPrint(sid: string, venderLot: string, qty: string, mfg: string, rid: string, spy: string, partNo: string, printer: string, count: number): Promise<string>;
|
||||
CancelJob(): Promise<string>;
|
||||
// Manage, Manual, Log Viewer
|
||||
OpenManage(): Promise<string>;
|
||||
CloseManage(): Promise<string>;
|
||||
OpenManual(): Promise<string>;
|
||||
OpenLogViewer(): Promise<string>;
|
||||
// Folder operations
|
||||
OpenProgramFolder(): Promise<string>;
|
||||
OpenLogFolder(): Promise<string>;
|
||||
OpenScreenshotFolder(): Promise<string>;
|
||||
OpenSavedDataFolder(): Promise<string>;
|
||||
// Picker Move methods
|
||||
GetPickerStatus(): Promise<string>;
|
||||
PickerMoveLeft(): Promise<string>;
|
||||
PickerMoveLeftWait(): Promise<string>;
|
||||
PickerMoveCenter(): Promise<string>;
|
||||
PickerMoveRightWait(): Promise<string>;
|
||||
PickerMoveRight(): Promise<string>;
|
||||
PickerJogStart(direction: string): Promise<string>;
|
||||
PickerJogStop(): Promise<string>;
|
||||
PickerStop(): Promise<string>;
|
||||
CancelVisionValidation(side: string): Promise<string>;
|
||||
PickerManagePosition(side: string): Promise<string>;
|
||||
PickerManageReturn(): Promise<string>;
|
||||
PickerZHome(): Promise<string>;
|
||||
PickerZZero(): Promise<string>;
|
||||
PickerTestPrint(side: string): Promise<string>;
|
||||
CanCloseManage(): Promise<string>;
|
||||
// Interlock methods
|
||||
ToggleInterlock(axisIndex: number, lockIndex: number): Promise<string>;
|
||||
GetInterlockList(): Promise<string>;
|
||||
// HW Status methods
|
||||
GetHWStatus(): Promise<string>;
|
||||
// History dialog
|
||||
OpenHistory(): Promise<string>;
|
||||
// History data
|
||||
GetHistoryData(startDate: string, endDate: string, search: string): Promise<string>;
|
||||
}
|
||||
};
|
||||
addEventListener(type: string, listener: (event: any) => void): void;
|
||||
|
||||
Reference in New Issue
Block a user