diff --git a/FrontEnd/App.tsx b/FrontEnd/App.tsx index 6c2ea43..7e84094 100644 --- a/FrontEnd/App.tsx +++ b/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; + 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 --- @@ -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.IDLE); const [currentRecipe, setCurrentRecipe] = useState({ 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(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(() => { @@ -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 ( - + } /> + } + /> + + {/* PickerMoveDialog - 전역에서 관리 */} + + + ); +} + +// 외부 App 컴포넌트 - AlertProvider로 감싸기 +export default function App() { + return ( + + ); } diff --git a/FrontEnd/communication.ts b/FrontEnd/communication.ts index d50d496..f26f98f 100644 --- a/FrontEnd/communication.ts +++ b/FrontEnd/communication.ts @@ -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 { + 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 { + 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' })); + }); + } } } diff --git a/FrontEnd/components/FunctionMenu.tsx b/FrontEnd/components/FunctionMenu.tsx index c54182d..03bae65 100644 --- a/FrontEnd/components/FunctionMenu.tsx +++ b/FrontEnd/components/FunctionMenu.tsx @@ -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 = ({ isOpen, onClose }) = const [activeSubmenu, setActiveSubmenu] = useState(null); const menuRef = useRef(null); const { showAlert } = useAlert(); + const { openPickerMove } = usePickerMove(); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { @@ -29,8 +31,6 @@ export const FunctionMenu: React.FC = ({ 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 = ({ isOpen, onClose }) = }; return ( + <> + {isOpen && (
= ({ isOpen, onClose }) =
{/* Manage */}
+ )} + ); }; diff --git a/FrontEnd/components/InitializeModal.tsx b/FrontEnd/components/InitializeModal.tsx index 97bf509..22bf815 100644 --- a/FrontEnd/components/InitializeModal.tsx +++ b/FrontEnd/components/InitializeModal.tsx @@ -28,7 +28,7 @@ export const InitializeModal: React.FC = ({ isOpen, onClos ]); const [isInitializing, setIsInitializing] = useState(false); const [errorMessage, setErrorMessage] = useState(null); - const [statusInterval, setStatusInterval] = useState(null); + const [statusInterval, setStatusInterval] = useState | null>(null); // Reset state when modal closes useEffect(() => { diff --git a/FrontEnd/components/PickerMoveDialog.tsx b/FrontEnd/components/PickerMoveDialog.tsx new file mode 100644 index 0000000..aafd137 --- /dev/null +++ b/FrontEnd/components/PickerMoveDialog.tsx @@ -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 = ({ isOpen, onClose }) => { + const { showAlert, showConfirm } = useAlert(); + const [status, setStatus] = useState({ + 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 ( +
+ {/* Backdrop */} +
+ + {/* Dialog */} +
+ {/* Header */} +
+
+ +

+ Picker(X) Movement and Management +

+
+ +
+ + {/* Content */} +
+ {/* Row 1: Jog Controls */} +
+ {/* Z Up */} + + + {/* X Left Jog */} + + + {/* Stop */} + + + {/* X Right Jog */} + + + {/* Z Down */} + +
+ + {/* Row 2: Position Buttons */} +
+ + + + + +
+ + {/* Row 3: Vision Cancel & Management Position */} +
+ + + + + +
+ + {/* Row 4: Z-Home, Print, Z-Zero */} +
+ + + + + +
+
+ + {/* Footer */} +
+ +
+
+
+ ); +}; diff --git a/FrontEnd/components/common/PanelHeader.tsx b/FrontEnd/components/common/PanelHeader.tsx index 06534a7..96879f6 100644 --- a/FrontEnd/components/common/PanelHeader.tsx +++ b/FrontEnd/components/common/PanelHeader.tsx @@ -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 = ({ title, icon: Icon }) => ( -
+export const PanelHeader: React.FC = ({ title, icon: Icon, className, children }) => ( +
@@ -15,5 +17,6 @@ export const PanelHeader: React.FC = ({ title, icon: Icon }) = {title}
+ {children}
); diff --git a/FrontEnd/components/layout/Footer.tsx b/FrontEnd/components/layout/Footer.tsx index d3b417f..fdca001 100644 --- a/FrontEnd/components/layout/Footer.tsx +++ b/FrontEnd/components/layout/Footer.tsx @@ -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 = ({ isHostConnected, robotTarget }) => { + const [hwStatus, setHwStatus] = useState([]); + + useEffect(() => { + // HW_STATUS_UPDATE 이벤트 구독 + const unsubscribe = comms.subscribe((msg: any) => { + if (msg?.type === 'HW_STATUS_UPDATE' && msg.data) { + setHwStatus(msg.data); + } + }); + + return () => { + unsubscribe(); + }; + }, []); + return (