// main.js const { app, BrowserWindow, Tray, Menu, nativeImage, dialog, ipcMain } = require('electron'); const path = require('path'); const io = require('socket.io-client'); const { desktopCapturer } = require('electron'); // 고유 직원 ID const EMPLOYEE_ID = "psn14020"; // 시그널링 서버 주소 const SIGNALING_SERVER = 'http://localhost:3001'; let tray = null; let mainWindow = null; let socket = null; let peerConnection = null; // 자동 시작 설정 app.setLoginItemSettings({ openAtLogin: true, openAsHidden: true }); // 백그라운드 창 (화면 공유용) function createHiddenWindow() { const win = new BrowserWindow({ show: true, webPreferences: { contextIsolation: true, preload: path.join(__dirname, 'preload.js') } }); win.loadFile('index.html'); return win; } // 트레이 아이콘 function createTray() { const iconPath = path.join(__dirname, 'resources', 'icon.png'); const icon = nativeImage.createFromPath(iconPath); tray = new Tray(icon || undefined); const contextMenu = Menu.buildFromTemplate([ { label: `직원 ID: ${EMPLOYEE_ID}`, enabled: false }, { label: '종료', click: () => app.quit() } ]); tray.setToolTip('원격 지원 에이전트 실행 중'); tray.setContextMenu(contextMenu); } // 🖥️ 사용 가능한 디스플레이 목록 전송 async function sendAvailableDisplays() { try { const sources = await desktopCapturer.getSources({ types: ['screen'] }); const displays = sources.map(source => ({ id: source.id, name: source.name, width: source.thumbnail.getSize().width, height: source.thumbnail.getSize().height })); socket.emit('availableDisplays', { displays }); } catch (err) { console.error('디스플레이 목록 가져오기 실패:', err); } } // 🔁 WebRTC 연결 설정 (offer 수신 후 호출) async function setupWebRTCConnection(offer, requestedDisplayId) { try { // 기존 연결 정리 if (peerConnection) { peerConnection.close(); } peerConnection = new RTCPeerConnection({ iceServers: [ { urls: 'stun:stun.l.google.com:19302' } // 프로덕션: TURN 서버 추가 권장 ] }); // 🖼️ 화면 스트림 캡처 const sources = await desktopCapturer.getSources({ types: ['screen'] }); const source = sources.find(s => s.id === requestedDisplayId); if (!source) throw new Error(`디스플레이 ${requestedDisplayId}를 찾을 수 없습니다.`); const stream = await navigator.mediaDevices.getUserMedia({ audio: false, video: { mandatory: { chromeMediaSource: 'desktop', chromeMediaSourceId: source.id, minWidth: 1280, minHeight: 720, maxWidth: 1920, maxHeight: 1080 } } }); // 스트림을 PeerConnection에 추가 stream.getTracks().forEach(track => { peerConnection.addTrack(track, stream); }); // Offer 설정 await peerConnection.setRemoteDescription(new RTCSessionDescription(offer)); // ✅ Answer SDP 생성 const answer = await peerConnection.createAnswer(); await peerConnection.setLocalDescription(answer); // Answer 전송 socket.emit('webrtcSignal', { targetId: EMPLOYEE_ID, answer }); // ICE candidate 전송 peerConnection.onicecandidate = (event) => { if (event.candidate) { socket.emit('webrtcSignal', { targetId: EMPLOYEE_ID, type: 'icecandidate', candidate: event.candidate }); } }; console.log('✅ WebRTC 연결 설정 완료. 화면 공유 시작됨.'); } catch (err) { console.error('❌ WebRTC 설정 실패:', err); dialog.showErrorBox('오류', '화면 공유 시작에 실패했습니다:\n' + err.message); } } // 🖱️ 입력 이벤트 처리 (robotjs 필요) function setupInputHandler() { // robotjs는 별도 설치 및 권한 필요 let robot; try { robot = require('robotjs'); } catch (e) { console.warn('robotjs 미설치 또는 로드 실패. 입력 제어 불가.'); return; } socket.on('inputEvent', ({ type, ...data }) => { try { if (type === 'mouse') { const { x, y, action } = data; robot.moveMouse(Math.round(x), Math.round(y)); if (action === 'click') { robot.mouseClick(); } } else if (type === 'keyboard') { const { key } = data; // 간단한 키 매핑 (보안상 복잡한 키는 제한 권장) if (key.length === 1 || ['Enter', 'Backspace', 'Tab', 'Escape'].includes(key)) { robot.keyTap(key.toLowerCase()); } } } catch (err) { console.error('입력 이벤트 처리 오류:', err); } }); } // 시그널링 연결 function connectToSignaling() { socket = io(SIGNALING_SERVER, { reconnection: true, reconnectionAttempts: Infinity, reconnectionDelay: 2000 }); socket.on('connect', () => { console.log('시그널링 서버 연결됨. 직원 등록 중...'); socket.emit('register', EMPLOYEE_ID); sendAvailableDisplays(); // 연결 시 디스플레이 목록 전송 }); socket.on('controlRequest', async ({ offer, displayId }) => { const result = await dialog.showMessageBox({ type: 'question', title: '원격 연결 요청', message: `관리자로부터 원격 제어 요청이 왔습니다.\n허용하시겠습니까?`, buttons: ['허용', '거부'], defaultId: 1, cancelId: 1 }); if (result.response === 0) { // ✅ 렌더러 프로세스에 WebRTC 시작 명령 전달 rendererWindow.webContents.send('start-webrtc', { offer, displayId, signalingServer: SIGNALING_SERVER, employeeId: EMPLOYEE_ID }); } else { socket.emit('webrtcSignal', { targetId: EMPLOYEE_ID, data: { type: 'reject' } }); } }); // WebRTC 신호 수신 (ICE candidate 등) socket.on('webrtcSignal', async ({ data }) => { if (!peerConnection) return; try { if (data.type === 'icecandidate' && data.candidate) { await peerConnection.addIceCandidate(new RTCIceCandidate(data.candidate)); } } catch (err) { console.error('ICE candidate 처리 오류:', err); } }); socket.on('disconnect', () => { console.log('시그널링 서버와 연결 끊김'); if (peerConnection) { peerConnection.close(); peerConnection = null; } }); socket.on('error', (err) => { console.error('소켓 오류:', err); }); // 입력 이벤트 리스너 설정 setupInputHandler(); } // 앱 준비 시 let rendererWindow = null; // 앱 준비 완료 app.whenReady().then(() => { rendererWindow = createHiddenWindow(); createTray(); connectToSignaling(); app.on('activate', () => { // macOS 대응 }); }); app.on('window-all-closed', () => { // Windows/Linux에서 종료하지 않음 }); ipcMain.on('input-event-from-renderer', (event, data) => { // 여기서 robotjs로 입력 실행 (메인 프로세스에서만 가능) try { const robot = require('robotjs'); if (data.type === 'mouse') { robot.moveMouse(Math.round(data.x), Math.round(data.y)); if (data.action === 'click') robot.mouseClick(); } else if (data.type === 'keyboard') { robot.keyTap(data.key); } } catch (e) { console.error('robotjs 오류:', e); } })