A
This commit is contained in:
75
agent-renderer.js
Normal file
75
agent-renderer.js
Normal file
@@ -0,0 +1,75 @@
|
||||
// agent-renderer.js — 브라우저 환경에서 실행됨 → WebRTC 사용 가능
|
||||
|
||||
let peerConnection = null;
|
||||
let socket = null;
|
||||
let currentStream = null;
|
||||
|
||||
// 메인 프로세스로부터 명령 수신 (IPC)
|
||||
console.log('electronAPI:', window.electronAPI);
|
||||
console.log('desktopCapturer:', window.desktopCapturer);
|
||||
window.electronAPI.receive('start-webrtc', async (payload) => {
|
||||
try {
|
||||
const { offer, displayId, signalingServer, employeeId } = payload;
|
||||
console.log('start webrtc');
|
||||
// 1. Socket 연결 (렌더러에서도 가능)
|
||||
socket = io(signalingServer);
|
||||
|
||||
// 2. 화면 스트림 캡처
|
||||
const sources = await window.desktopCapturer.getSources({ types: ['screen'] });
|
||||
const source = sources.find(s => s.id === displayId);
|
||||
if (!source) throw new Error('디스플레이 없음');
|
||||
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: false,
|
||||
video: {
|
||||
mandatory: {
|
||||
chromeMediaSource: 'desktop',
|
||||
chromeMediaSourceId: source.id,
|
||||
minWidth: 1280,
|
||||
minHeight: 720
|
||||
}
|
||||
}
|
||||
});
|
||||
currentStream = stream;
|
||||
|
||||
// 3. WebRTC 연결
|
||||
peerConnection = new RTCPeerConnection({
|
||||
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
|
||||
});
|
||||
|
||||
stream.getTracks().forEach(track => peerConnection.addTrack(track, stream));
|
||||
|
||||
await peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
|
||||
const answer = await peerConnection.createAnswer();
|
||||
await peerConnection.setLocalDescription(answer);
|
||||
|
||||
// 4. Answer 전송
|
||||
socket.emit('webrtcSignal', {
|
||||
targetId: employeeId,
|
||||
answer
|
||||
});
|
||||
|
||||
// 5. ICE candidate
|
||||
peerConnection.onicecandidate = (e) => {
|
||||
if (e.candidate) {
|
||||
socket.emit('webrtcSignal', {
|
||||
targetId: employeeId,
|
||||
type: 'icecandidate', candidate: e.candidate
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 6. 입력 이벤트 수신 (robotjs는 메인 프로세스에서 실행 권장)
|
||||
socket.on('inputEvent', (data) => {
|
||||
window.electronApi.send('input-event-from-renderer', data);
|
||||
});
|
||||
|
||||
console.log('✅ WebRTC 시작됨 (렌더러)');
|
||||
} catch (err) {
|
||||
console.error('❌ WebRTC 실패:', err);
|
||||
window.electronApi.send('webrtc-error', err.message);
|
||||
}
|
||||
});
|
||||
|
||||
// desktopCapturer 사용을 위해 preload 필요
|
||||
// → 다음 단계에서 preload.js 설정
|
||||
11
index.html
Normal file
11
index.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Remote Agent Renderer</title>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 이 창은 숨김 처리됨. WebRTC만 실행 -->
|
||||
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
|
||||
<script src="./agent-renderer.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
19
jsconfig.json
Normal file
19
jsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "esnext",
|
||||
"baseUrl": "./",
|
||||
"moduleResolution": "node",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
]
|
||||
}
|
||||
}
|
||||
205
main.js
205
main.js
@@ -1,39 +1,40 @@
|
||||
// main.js
|
||||
const { app, BrowserWindow, Tray, Menu, nativeImage, ipcMain } = require('electron');
|
||||
const { app, BrowserWindow, Tray, Menu, nativeImage, dialog, ipcMain } = require('electron');
|
||||
const path = require('path');
|
||||
const io = require('socket.io-client');
|
||||
const os = require('os');
|
||||
const { desktopCapturer } = require('electron');
|
||||
|
||||
// 고유 직원 ID (실제로는 설정 파일, 환경변수, 또는 도메인 계정 연동 권장)
|
||||
// 고유 직원 ID
|
||||
const EMPLOYEE_ID = "psn14020";
|
||||
|
||||
// 시그널링 서버 주소
|
||||
const SIGNALING_SERVER = 'http://localhost:3001'; // 실제 서버로 변경
|
||||
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() {
|
||||
mainWindow = new BrowserWindow({
|
||||
show: false,
|
||||
const win = new BrowserWindow({
|
||||
show: true,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
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);
|
||||
@@ -46,11 +47,122 @@ function createTray() {
|
||||
tray.setContextMenu(contextMenu);
|
||||
}
|
||||
|
||||
// WebRTC 연결 준비
|
||||
function setupWebRTC() {
|
||||
// 실제 구현 시 RTCPeerConnection 생성, 스트림 처리 등 포함
|
||||
// 여기선 간략히 placeholder
|
||||
console.log('WebRTC 준비됨');
|
||||
// 🖥️ 사용 가능한 디스플레이 목록 전송
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 시그널링 연결
|
||||
@@ -64,11 +176,10 @@ function connectToSignaling() {
|
||||
socket.on('connect', () => {
|
||||
console.log('시그널링 서버 연결됨. 직원 등록 중...');
|
||||
socket.emit('register', EMPLOYEE_ID);
|
||||
sendAvailableDisplays(); // 연결 시 디스플레이 목록 전송
|
||||
});
|
||||
|
||||
socket.on('controlRequest', async ({ from, offer }) => {
|
||||
// 🔔 사용자에게 알림 (간단한 확인 창)
|
||||
const { dialog } = require('electron');
|
||||
socket.on('controlRequest', async ({ offer, displayId }) => {
|
||||
const result = await dialog.showMessageBox({
|
||||
type: 'question',
|
||||
title: '원격 연결 요청',
|
||||
@@ -79,43 +190,73 @@ function connectToSignaling() {
|
||||
});
|
||||
|
||||
if (result.response === 0) {
|
||||
// 허용 → WebRTC 연결 시작
|
||||
socket.emit('webrtcSignal', {
|
||||
targetId: EMPLOYEE_ID,
|
||||
data: { type: 'answer', sdp: '...' } // 실제 SDP 생성 필요
|
||||
// ✅ 렌더러 프로세스에 WebRTC 시작 명령 전달
|
||||
rendererWindow.webContents.send('start-webrtc', {
|
||||
offer,
|
||||
displayId,
|
||||
signalingServer: SIGNALING_SERVER,
|
||||
employeeId: EMPLOYEE_ID
|
||||
});
|
||||
setupWebRTC();
|
||||
} else {
|
||||
socket.emit('webrtcSignal', { targetId: EMPLOYEE_ID, data: { type: 'reject' } });
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('webrtcSignal', ({ data }) => {
|
||||
// ICE candidate, answer 등 처리
|
||||
console.log('WebRTC 신호 수신:', data);
|
||||
// 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(() => {
|
||||
createHiddenWindow();
|
||||
rendererWindow = createHiddenWindow();
|
||||
createTray();
|
||||
connectToSignaling();
|
||||
|
||||
app.on('activate', () => {
|
||||
// macOS에서 dock 클릭 시 동작 (필요 시 창 표시)
|
||||
// macOS 대응
|
||||
});
|
||||
});
|
||||
|
||||
// 모든 창 닫힘 (Windows/Linux)
|
||||
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);
|
||||
}
|
||||
})
|
||||
17
preload.js
17
preload.js
@@ -3,4 +3,21 @@ const { contextBridge, ipcRenderer } = require('electron');
|
||||
|
||||
contextBridge.exposeInMainWorld('agentAPI', {
|
||||
getEmployeeId: () => ipcRenderer.invoke('get-employee-id')
|
||||
});
|
||||
// preload.js
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
// 메인 → 렌더러 메시지 수신
|
||||
receive: (channel, func) => {
|
||||
ipcRenderer.on(channel, (event, ...args) => func(...args));
|
||||
},
|
||||
// 렌더러 → 메인 메시지 전송
|
||||
send: (channel, data) => {
|
||||
ipcRenderer.send(channel, data);
|
||||
}
|
||||
});
|
||||
|
||||
// desktopCapturer 노출 (보안 주의: 신뢰 가능한 코드만)
|
||||
contextBridge.exposeInMainWorld('desktopCapturer', {
|
||||
getSources: (options) => require('electron').desktopCapturer.getSources(options)
|
||||
});
|
||||
Reference in New Issue
Block a user