Files
gravity_web/public/js/app.js

344 lines
12 KiB
JavaScript

/**
* Gravity Web — App 초기화 및 WebSocket 관리
*/
(function () {
// ─── 컴포넌트 초기화 ──────────────────────────────────
const sessionPanel = new SessionPanel();
const chatPanel = new ChatPanel();
const mirrorPanel = new MirrorPanel();
let ws = null;
let reconnectTimer = null;
let currentView = 'chat'; // 'chat' | 'mirror'
const WS_URL = `ws://${location.host}/ws`;
// ─── DOM 레퍼런스 ─────────────────────────────────────
const connectionStatus = document.getElementById('connectionStatus');
const statusDot = connectionStatus.querySelector('.status-dot');
const statusText = connectionStatus.querySelector('.status-text');
const addSessionBtn = document.getElementById('addSessionBtn');
const addSessionModal = document.getElementById('addSessionModal');
const closeModal = document.getElementById('closeModal');
const cancelModal = document.getElementById('cancelModal');
const confirmAddSession = document.getElementById('confirmAddSession');
const sessionNameInput = document.getElementById('sessionName');
const sessionHostInput = document.getElementById('sessionHost');
const sessionPortInput = document.getElementById('sessionPort');
const screenshotBtn = document.getElementById('screenshotBtn');
const reconnectBtn = document.getElementById('reconnectBtn');
const screenshotOverlay = document.getElementById('screenshotOverlay');
const screenshotImage = document.getElementById('screenshotImage');
const closeScreenshot = document.getElementById('closeScreenshot');
const tabChat = document.getElementById('tabChat');
const tabMirror = document.getElementById('tabMirror');
const chatMessagesWrap = document.getElementById('chatMessages');
const chatInputArea = document.querySelector('.chat-input-area');
const mirrorHint = document.getElementById('mirrorHint');
// ─── WebSocket 연결 ───────────────────────────────────
function connectWebSocket() {
ws = new WebSocket(WS_URL);
ws.onopen = () => {
setConnectionStatus('connected', '서버 연결됨');
if (reconnectTimer) {
clearTimeout(reconnectTimer);
reconnectTimer = null;
}
};
ws.onclose = () => {
setConnectionStatus('error', '연결 끊김');
scheduleReconnect();
};
ws.onerror = () => {
setConnectionStatus('error', '연결 오류');
};
ws.onmessage = (event) => {
try {
const msg = JSON.parse(event.data);
handleMessage(msg);
} catch (e) {
console.error('WS 메시지 파싱 오류:', e);
}
};
}
function scheduleReconnect() {
if (reconnectTimer) return;
reconnectTimer = setTimeout(() => {
reconnectTimer = null;
connectWebSocket();
}, 3000);
}
function sendWs(msg) {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(msg));
}
}
// ─── 서버 메시지 핸들러 ───────────────────────────────
function handleMessage(msg) {
switch (msg.type) {
case 'sessions_list':
sessionPanel.update(msg.sessions);
break;
case 'session_switched':
sessionPanel.setActive(msg.sessionId);
chatPanel.showSession(msg.session);
break;
case 'chat_update':
if (msg.sessionId === sessionPanel.activeSessionId) {
chatPanel.updateChat(msg.messages);
}
break;
case 'message_sent':
if (!msg.success) {
showToast(`전송 실패: ${msg.error}`, 'error');
}
break;
case 'screenshot':
screenshotImage.src = `data:image/jpeg;base64,${msg.data}`;
screenshotOverlay.style.display = 'flex';
break;
case 'screencast_frame':
if (msg.sessionId === sessionPanel.activeSessionId) {
mirrorPanel.updateFrame(msg.data, msg.metadata);
if (mirrorHint) mirrorHint.style.display = 'none';
}
break;
case 'screencast_started':
if (!msg.success) {
showToast('Screencast 시작 실패', 'error');
}
break;
case 'screencast_stopped':
break;
case 'action_clicked':
// 서버에서의 확인 응답
break;
case 'auto_discovered':
showToast(`Antigravity 자동 감지: ${msg.session.name}`, 'success');
// 활성 세션이 없으면 자동 전환
if (!sessionPanel.activeSessionId) {
sendWs({ type: 'switch_session', sessionId: msg.session.id });
}
break;
case 'session_lost':
showToast('Antigravity 연결 소실', 'warning');
if (sessionPanel.activeSessionId === msg.sessionId) {
chatPanel.showEmpty();
sessionPanel.setActive(null);
}
break;
case 'error':
showToast(msg.message, 'error');
break;
}
}
// ─── 세션 패널 이벤트 ─────────────────────────────────
sessionPanel.onSessionSelect = (sessionId) => {
sendWs({ type: 'switch_session', sessionId });
};
sessionPanel.onSessionRemove = async (sessionId) => {
try {
const res = await fetch(`/api/sessions/${sessionId}`, { method: 'DELETE' });
if (res.ok) {
if (sessionPanel.activeSessionId === sessionId) {
chatPanel.showEmpty();
sessionPanel.setActive(null);
}
showToast('세션이 제거되었습니다', 'success');
}
} catch (e) {
showToast('세션 제거 실패', 'error');
}
};
// ─── 채팅 패널 이벤트 ─────────────────────────────────
chatPanel.onSendMessage = (text) => {
sendWs({
type: 'send_message',
sessionId: sessionPanel.activeSessionId,
text,
});
};
chatPanel.onActionClick = (button) => {
sendWs({
type: 'click_action',
sessionId: sessionPanel.activeSessionId,
label: button.label,
x: button.x,
y: button.y,
});
showToast(`"${button.label}" 클릭`, 'success');
};
// ─── 미러 패널 이벤트 ─────────────────────────────
mirrorPanel.onStartScreencast = (sessionId) => {
sendWs({ type: 'start_screencast', sessionId });
};
mirrorPanel.onStopScreencast = (sessionId) => {
sendWs({ type: 'stop_screencast', sessionId });
};
mirrorPanel.onInputEvent = (sessionId, event) => {
sendWs({ type: 'input_event', sessionId, event });
};
// ─── 탭 전환 (Chat ↔ Mirror) ────────────────────────
function switchView(view) {
currentView = view;
// 탭 UI
tabChat.classList.toggle('active', view === 'chat');
tabMirror.classList.toggle('active', view === 'mirror');
// 채팅 영역 표시/숨기기
chatMessagesWrap.style.display = view === 'chat' ? '' : 'none';
if (chatInputArea) chatInputArea.style.display = view === 'chat' ? '' : 'none';
if (view === 'mirror') {
if (mirrorHint) mirrorHint.style.display = '';
mirrorPanel.start(sessionPanel.activeSessionId);
} else {
mirrorPanel.stop();
}
}
tabChat?.addEventListener('click', () => switchView('chat'));
tabMirror?.addEventListener('click', () => switchView('mirror'));
// ─── 모달 ─────────────────────────────────────────────
function showModal() {
addSessionModal.style.display = 'flex';
sessionNameInput.value = '';
sessionHostInput.value = 'localhost';
sessionPortInput.value = '9000';
setTimeout(() => sessionNameInput.focus(), 100);
}
function hideModal() {
addSessionModal.style.display = 'none';
}
async function addSession() {
const name = sessionNameInput.value.trim();
const host = sessionHostInput.value.trim();
const port = parseInt(sessionPortInput.value, 10);
if (!name) {
sessionNameInput.focus();
return;
}
hideModal();
try {
const res = await fetch('/api/sessions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, host, cdpPort: port }),
});
const session = await res.json();
if (session.status === 'connected') {
showToast(`${name} 연결 성공`, 'success');
} else {
showToast(`${name} 연결 실패: ${session.error || '알 수 없는 오류'}`, 'error');
}
} catch (e) {
showToast('세션 추가 실패', 'error');
}
}
addSessionBtn.addEventListener('click', showModal);
closeModal.addEventListener('click', hideModal);
cancelModal.addEventListener('click', hideModal);
confirmAddSession.addEventListener('click', addSession);
// Enter로 모달 확인
addSessionModal.addEventListener('keydown', (e) => {
if (e.key === 'Enter') addSession();
if (e.key === 'Escape') hideModal();
});
// 배경 클릭으로 모달 닫기
addSessionModal.addEventListener('click', (e) => {
if (e.target === addSessionModal) hideModal();
});
// ─── 스크린샷 ─────────────────────────────────────────
screenshotBtn.addEventListener('click', () => {
sendWs({ type: 'get_screenshot', sessionId: sessionPanel.activeSessionId });
});
closeScreenshot.addEventListener('click', () => {
screenshotOverlay.style.display = 'none';
});
screenshotOverlay.addEventListener('click', (e) => {
if (e.target === screenshotOverlay) {
screenshotOverlay.style.display = 'none';
}
});
// ─── 재연결 ───────────────────────────────────────────
reconnectBtn.addEventListener('click', async () => {
const id = sessionPanel.activeSessionId;
if (!id) return;
try {
const res = await fetch(`/api/sessions/${id}/reconnect`, { method: 'POST' });
const result = await res.json();
if (result.success) {
showToast('재연결 성공', 'success');
} else {
showToast(`재연결 실패: ${result.error}`, 'error');
}
} catch {
showToast('재연결 실패', 'error');
}
});
// ─── 유틸리티 ─────────────────────────────────────────
function setConnectionStatus(status, text) {
statusDot.className = `status-dot ${status}`;
statusText.textContent = text;
}
function showToast(message, type = '') {
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
}
// ─── 시작 ─────────────────────────────────────────────
connectWebSocket();
})();