// MiraBox StreamDock 플러그인 - 브라우저 기반 console.log('🚀 Streamingle 플러그인 시작 (브라우저 기반)'); // Global variables let websocket = null; let buttonContexts = new Map(); // 각 버튼의 컨텍스트별 설정 저장 let unitySocket = null; let isUnityConnected = false; let cameraList = []; // 카메라 목록 저장 let itemList = []; // 아이템 목록 저장 let eventList = []; // 이벤트 목록 저장 let avatarOutfitList = []; // 아바타 의상 목록 저장 // StreamDock 연결 함수 (브라우저 기반) function connectElgatoStreamDeckSocket(inPort, inUUID, inEvent, inInfo, inActionInfo) { console.log('🔌 StreamDock 연결 시작 (브라우저)'); console.log('📡 포트:', inPort, 'UUID:', inUUID, '이벤트:', inEvent); // Parse action info to get initial settings try { if (inActionInfo) { const actionInfo = JSON.parse(inActionInfo); const context = actionInfo.context; const settings = actionInfo.payload?.settings || {}; // 각 컨텍스트별로 설정 저장 buttonContexts.set(context, settings); console.log('⚙️ 초기 설정 저장:', settings); } } catch (error) { console.error('❌ ActionInfo 파싱 오류:', error); } // WebSocket 연결 if (!websocket) { websocket = new WebSocket('ws://localhost:' + inPort); websocket.onopen = function() { console.log('✅ StreamDock 연결됨 (브라우저)'); // StreamDock에 등록 websocket.send(JSON.stringify({ event: inEvent, uuid: inUUID })); // Unity 연결 시도 setTimeout(() => { connectToUnity(); }, 1000); // Unity 연결 재시도 (5초마다) setInterval(() => { if (!isUnityConnected) { console.log('🔄 Unity 재연결 시도...'); connectToUnity(); } }, 5000); }; websocket.onmessage = function(evt) { try { const jsonObj = JSON.parse(evt.data); console.log('📨 StreamDock 메시지 수신:', jsonObj.event); console.log('📋 전체 메시지 데이터:', JSON.stringify(jsonObj, null, 2)); switch(jsonObj.event) { case 'didReceiveSettings': if (jsonObj.payload && jsonObj.context) { const newSettings = jsonObj.payload.settings || {}; // actionType이 없으면 action UUID로 판단해서 강제 설정 if (!newSettings.actionType && jsonObj.action) { console.log('⚠️ actionType 없음! action UUID로 재설정:', jsonObj.action); if (jsonObj.action === 'com.mirabox.streamingle.optitrack_marker_toggle') { newSettings.actionType = 'optitrack_marker_toggle'; console.log('✅ 마커 버튼으로 강제 설정됨'); // StreamDock SDK에 저장 if (websocket) { const setSettingsMessage = { event: 'setSettings', context: jsonObj.context, payload: newSettings }; websocket.send(JSON.stringify(setSettingsMessage)); console.log('💾 강제 설정 저장:', newSettings); } } else if (jsonObj.action === 'com.mirabox.streamingle.motion_recording_toggle') { newSettings.actionType = 'motion_recording_toggle'; console.log('🎬 모션 녹화 버튼으로 강제 설정됨'); // StreamDock SDK에 저장 if (websocket) { const setSettingsMessage = { event: 'setSettings', context: jsonObj.context, payload: newSettings }; websocket.send(JSON.stringify(setSettingsMessage)); console.log('💾 강제 설정 저장:', newSettings); } } else if (jsonObj.action === 'com.mirabox.streamingle.optitrack_reconnect') { newSettings.actionType = 'optitrack_reconnect'; console.log('🔄 OptiTrack 재접속 버튼으로 강제 설정됨'); // StreamDock SDK에 저장 if (websocket) { const setSettingsMessage = { event: 'setSettings', context: jsonObj.context, payload: newSettings }; websocket.send(JSON.stringify(setSettingsMessage)); console.log('💾 강제 설정 저장:', newSettings); } } else if (jsonObj.action === 'com.mirabox.streamingle.facial_motion_reconnect') { newSettings.actionType = 'facial_motion_reconnect'; console.log('😊 Facial Motion 재접속 버튼으로 강제 설정됨'); // StreamDock SDK에 저장 if (websocket) { const setSettingsMessage = { event: 'setSettings', context: jsonObj.context, payload: newSettings }; websocket.send(JSON.stringify(setSettingsMessage)); console.log('💾 강제 설정 저장:', newSettings); } } else if (jsonObj.action === 'com.mirabox.streamingle.screenshot') { newSettings.actionType = 'screenshot'; console.log('📸 스크린샷 버튼으로 강제 설정됨'); // StreamDock SDK에 저장 if (websocket) { const setSettingsMessage = { event: 'setSettings', context: jsonObj.context, payload: newSettings }; websocket.send(JSON.stringify(setSettingsMessage)); console.log('💾 강제 설정 저장:', newSettings); } } else if (jsonObj.action === 'com.mirabox.streamingle.screenshot_alpha') { newSettings.actionType = 'screenshot_alpha'; console.log('📷 알파 스크린샷 버튼으로 강제 설정됨'); // StreamDock SDK에 저장 if (websocket) { const setSettingsMessage = { event: 'setSettings', context: jsonObj.context, payload: newSettings }; websocket.send(JSON.stringify(setSettingsMessage)); console.log('💾 강제 설정 저장:', newSettings); } } else if (jsonObj.action === 'com.mirabox.streamingle.item') { newSettings.actionType = 'item'; } else if (jsonObj.action === 'com.mirabox.streamingle.event') { newSettings.actionType = 'event'; } else if (jsonObj.action === 'com.mirabox.streamingle.avatar_outfit') { newSettings.actionType = 'avatar_outfit'; } else { newSettings.actionType = 'camera'; } } buttonContexts.set(jsonObj.context, newSettings); console.log('⚙️ 설정 업데이트:', newSettings); updateButtonTitle(jsonObj.context); } break; case 'willAppear': console.log('============================================'); console.log('👀 버튼 나타남 이벤트'); console.log('🔍 Context:', jsonObj.context); console.log('🔍 Action UUID:', jsonObj.action); let settings = jsonObj.payload?.settings || {}; console.log('⚙️ 초기 설정:', JSON.stringify(settings)); // action UUID로 actionType 결정 if (jsonObj.action === 'com.mirabox.streamingle.item') { settings.actionType = 'item'; console.log('🎯 아이템 컨트롤러 등록'); } else if (jsonObj.action === 'com.mirabox.streamingle.event') { settings.actionType = 'event'; console.log('🎯 이벤트 컨트롤러 등록'); } else if (jsonObj.action === 'com.mirabox.streamingle.avatar_outfit') { settings.actionType = 'avatar_outfit'; console.log('👗 아바타 의상 컨트롤러 등록'); } else if (jsonObj.action === 'com.mirabox.streamingle.optitrack_marker_toggle') { settings.actionType = 'optitrack_marker_toggle'; console.log('✅✅✅ MARKER BUTTON DETECTED ✅✅✅'); console.log('✅ This is the OptiTrack Marker Toggle button!'); console.log('✅ When clicked, it should send: toggle_optitrack_markers'); // 기본 제목 설정 setButtonTitle(jsonObj.context, '마커\nON'); } else if (jsonObj.action === 'com.mirabox.streamingle.motion_recording_toggle') { settings.actionType = 'motion_recording_toggle'; console.log('🎬🎬🎬 MOTION RECORDING BUTTON DETECTED 🎬🎬🎬'); console.log('🎬 This is the Motion Recording Toggle button!'); console.log('🎬 When clicked, it should send: toggle_motion_recording'); // 기본 제목 설정 setButtonTitle(jsonObj.context, '녹화\nOFF'); } else if (jsonObj.action === 'com.mirabox.streamingle.optitrack_reconnect') { settings.actionType = 'optitrack_reconnect'; console.log('🔄🔄🔄 OPTITRACK RECONNECT BUTTON DETECTED 🔄🔄🔄'); console.log('🔄 This is the OptiTrack Reconnect button!'); console.log('🔄 When clicked, it should send: reconnect_optitrack'); // 기본 제목 설정 setButtonTitle(jsonObj.context, '옵티트랙\n재접속'); } else if (jsonObj.action === 'com.mirabox.streamingle.facial_motion_reconnect') { settings.actionType = 'facial_motion_reconnect'; console.log('😊😊😊 FACIAL MOTION RECONNECT BUTTON DETECTED 😊😊😊'); console.log('😊 This is the Facial Motion Reconnect button!'); console.log('😊 When clicked, it should send: reconnect_facial_motion'); // 기본 제목 설정 setButtonTitle(jsonObj.context, '페이셜\n재접속'); } else if (jsonObj.action === 'com.mirabox.streamingle.screenshot') { settings.actionType = 'screenshot'; console.log('📸📸📸 SCREENSHOT BUTTON DETECTED 📸📸📸'); console.log('📸 This is the Screenshot button!'); console.log('📸 When clicked, it should send: capture_screenshot'); // 기본 제목 설정 setButtonTitle(jsonObj.context, '스크린샷'); } else if (jsonObj.action === 'com.mirabox.streamingle.screenshot_alpha') { settings.actionType = 'screenshot_alpha'; console.log('📷📷📷 ALPHA SCREENSHOT BUTTON DETECTED 📷📷📷'); console.log('📷 This is the Alpha Screenshot button!'); console.log('📷 When clicked, it should send: capture_alpha_screenshot'); // 기본 제목 설정 setButtonTitle(jsonObj.context, '알파\n스크린샷'); } else { settings.actionType = 'camera'; console.log('📹 카메라 컨트롤러 등록 (기본값)'); console.log('📹 This button will send camera switch messages'); } console.log('🎯 최종 actionType:', settings.actionType); buttonContexts.set(jsonObj.context, settings); // StreamDock SDK에도 설정 저장 if (websocket) { const setSettingsMessage = { event: 'setSettings', context: jsonObj.context, payload: settings }; websocket.send(JSON.stringify(setSettingsMessage)); console.log('💾 StreamDock SDK에 설정 저장:', settings); } console.log('💾 설정 저장 완료'); console.log('============================================'); updateButtonTitle(jsonObj.context); // Unity가 이미 연결되어 있다면 Property Inspector에 상태 전송 if (isUnityConnected) { const settings = getCurrentSettings(jsonObj.context); const actionType = settings.actionType || 'camera'; let actionUUID = 'com.mirabox.streamingle.camera'; if (actionType === 'item') { actionUUID = 'com.mirabox.streamingle.item'; } else if (actionType === 'event') { actionUUID = 'com.mirabox.streamingle.event'; } else if (actionType === 'avatar_outfit') { actionUUID = 'com.mirabox.streamingle.avatar_outfit'; } console.log('📤 willAppear에서 Unity 연결 상태 전송:', actionType, actionUUID); sendToPropertyInspector(jsonObj.context, 'unity_connected', { connected: true }, actionUUID); // 아바타 의상 컨트롤러인 경우 데이터도 함께 전송 if (actionType === 'avatar_outfit') { console.log('📤 willAppear에서 아바타 데이터 전송:', avatarOutfitList.length, '개'); if (avatarOutfitList.length > 0) { sendToPropertyInspector(jsonObj.context, 'avatar_outfit_list', { avatars: avatarOutfitList, currentIndex: 0 }, actionUUID); } else { // 아바타 목록이 없으면 Unity에 요청 console.log('📤 willAppear에서 아바타 목록 요청'); if (unitySocket && unitySocket.readyState === WebSocket.OPEN) { const message = JSON.stringify({ type: 'get_avatar_outfit_list' }); unitySocket.send(message); console.log('📤 Unity에 아바타 목록 요청 전송 (willAppear)'); } } } } break; case 'keyUp': console.log('🔘 버튼 클릭됨!'); console.log('🔍 Action UUID:', jsonObj.action); handleButtonClick(jsonObj.context, jsonObj.action); break; case 'sendToPlugin': console.log('📨 sendToPlugin 이벤트 수신'); console.log(' - context:', jsonObj.context); console.log(' - action:', jsonObj.action); console.log(' - payload:', jsonObj.payload); handlePropertyInspectorMessage(jsonObj.payload, jsonObj.context, jsonObj.action); break; case 'setSettings': console.log('💾 setSettings 이벤트 수신:', jsonObj.payload, 'context:', jsonObj.context); if (jsonObj.payload && jsonObj.context) { const currentSettings = getCurrentSettings(jsonObj.context); console.log('📋 저장 전 설정:', currentSettings); // actionType을 유지하면서 설정 업데이트 const updatedSettings = { ...currentSettings, ...jsonObj.payload }; setCurrentSettings(jsonObj.context, updatedSettings); const newSettings = getCurrentSettings(jsonObj.context); console.log('✅ 설정 저장 완료:', newSettings); console.log('🎯 액션 타입:', newSettings.actionType); updateButtonTitle(jsonObj.context); } break; default: console.log('❓ 알 수 없는 이벤트:', jsonObj.event); break; } } catch (error) { console.error('❌ 메시지 파싱 오류:', error); } }; websocket.onclose = function() { console.log('❌ StreamDock 연결 끊어짐'); websocket = null; }; websocket.onerror = function(error) { console.error('❌ StreamDock 연결 오류:', error); }; } } // Unity 연결 함수 (브라우저 네이티브 WebSocket 사용) function connectToUnity() { console.log('🔌 Unity 연결 시도 (브라우저)...'); if (unitySocket) { unitySocket.close(); } try { unitySocket = new WebSocket('ws://localhost:10701'); unitySocket.onopen = function() { isUnityConnected = true; console.log('✅ Unity 연결 성공 (브라우저)!'); // 카메라 목록 요청 setTimeout(() => { const message = JSON.stringify({ type: 'get_camera_list' }); unitySocket.send(message); console.log('📋 카메라 목록 요청:', message); }, 100); // 아이템 목록 요청 setTimeout(() => { const message = JSON.stringify({ type: 'get_item_list' }); unitySocket.send(message); console.log('📋 아이템 목록 요청:', message); }, 200); // 이벤트 목록 요청 setTimeout(() => { const message = JSON.stringify({ type: 'get_event_list' }); unitySocket.send(message); console.log('📋 이벤트 목록 요청:', message); }, 300); // 아바타 의상 목록 요청 setTimeout(() => { const message = JSON.stringify({ type: 'get_avatar_outfit_list' }); unitySocket.send(message); console.log('📋 아바타 의상 목록 요청:', message); }, 400); }; unitySocket.onmessage = function(event) { try { const data = JSON.parse(event.data); console.log('📨 Unity 메시지 수신:', data.type); console.log('📋 Unity 메시지 전체 데이터:', JSON.stringify(data, null, 2)); handleUnityMessage(data); } catch (error) { console.error('❌ Unity 메시지 파싱 오류:', error); } }; unitySocket.onclose = function() { isUnityConnected = false; unitySocket = null; // 소켓을 null로 설정 console.log('❌ Unity 연결 끊어짐'); // Property Inspector들에게 Unity 연결 해제 알림 for (const context of buttonContexts.keys()) { // context별로 적절한 action UUID 결정 const settings = getCurrentSettings(context); const actionType = settings.actionType || 'camera'; let actionUUID = 'com.mirabox.streamingle.camera'; if (actionType === 'item') { actionUUID = 'com.mirabox.streamingle.item'; } else if (actionType === 'event') { actionUUID = 'com.mirabox.streamingle.event'; } else if (actionType === 'avatar_outfit') { actionUUID = 'com.mirabox.streamingle.avatar_outfit'; } sendToPropertyInspector(context, 'unity_connected', { connected: false }, actionUUID); } }; unitySocket.onerror = function(error) { isUnityConnected = false; unitySocket = null; // 소켓을 null로 설정 console.error('❌ Unity 연결 오류:', error); console.log('🔍 Unity가 실행 중인지 확인하세요 (포트 10701)'); }; } catch (error) { console.error('❌ Unity 연결 설정 오류:', error); } } // 버튼 클릭 처리 function handleButtonClick(context, actionUUID) { console.log('🎯 버튼 클릭 처리 시작'); console.log('📍 컨텍스트:', context); console.log('📍 Action UUID:', actionUUID); console.log('🔌 Unity 연결 상태:', isUnityConnected); if (!isUnityConnected || !unitySocket) { console.log('🔄 Unity 연결되지 않음, 재연결 시도...'); connectToUnity(); // 연결 후 잠시 대기한 후 다시 시도 setTimeout(() => { if (isUnityConnected && unitySocket) { handleButtonClick(context, actionUUID); } else { console.error('❌ Unity 재연결 실패'); } }, 1000); return; } // context별 settings 사용 const settings = getCurrentSettings(context); let actionType = settings.actionType; // actionType이 없으면 action UUID로 판단 if (!actionType && actionUUID) { console.log('⚠️ actionType 없음! Action UUID로 결정:', actionUUID); if (actionUUID === 'com.mirabox.streamingle.optitrack_marker_toggle') { actionType = 'optitrack_marker_toggle'; console.log('✅ 마커 버튼으로 인식'); } else if (actionUUID === 'com.mirabox.streamingle.motion_recording_toggle') { actionType = 'motion_recording_toggle'; console.log('🎬 모션 녹화 버튼으로 인식'); } else if (actionUUID === 'com.mirabox.streamingle.optitrack_reconnect') { actionType = 'optitrack_reconnect'; console.log('🔄 OptiTrack 재접속 버튼으로 인식'); } else if (actionUUID === 'com.mirabox.streamingle.facial_motion_reconnect') { actionType = 'facial_motion_reconnect'; console.log('😊 Facial Motion 재접속 버튼으로 인식'); } else if (actionUUID === 'com.mirabox.streamingle.screenshot') { actionType = 'screenshot'; console.log('📸 스크린샷 버튼으로 인식'); } else if (actionUUID === 'com.mirabox.streamingle.screenshot_alpha') { actionType = 'screenshot_alpha'; console.log('📷 알파 스크린샷 버튼으로 인식'); } else if (actionUUID === 'com.mirabox.streamingle.item') { actionType = 'item'; } else if (actionUUID === 'com.mirabox.streamingle.event') { actionType = 'event'; } else if (actionUUID === 'com.mirabox.streamingle.avatar_outfit') { actionType = 'avatar_outfit'; } else { actionType = 'camera'; } // 설정에 저장 settings.actionType = actionType; setCurrentSettings(context, settings); } if (!actionType) { actionType = 'camera'; // 최종 폴백 } console.log('╔════════════════════════════════════════╗'); console.log('║ BUTTON CLICKED - DEBUG INFO ║'); console.log('╠════════════════════════════════════════╣'); console.log(' Context:', context); console.log(' Action UUID:', actionUUID); console.log(' ActionType:', actionType); console.log(' All Settings:', JSON.stringify(settings)); console.log('╚════════════════════════════════════════╝'); switch (actionType) { case 'camera': console.log('➡️ Routing to: CAMERA handler'); console.log(' Will send: {"type":"switch_camera","data":{...}}'); handleCameraAction(settings); break; case 'item': console.log('➡️ Routing to: ITEM handler'); handleItemAction(settings); break; case 'event': console.log('➡️ Routing to: EVENT handler'); handleEventAction(settings); break; case 'avatar_outfit': console.log('➡️ Routing to: AVATAR OUTFIT handler'); handleAvatarOutfitAction(settings); break; case 'optitrack_marker_toggle': console.log('➡️ Routing to: MARKER TOGGLE handler'); console.log(' Will send: {"type":"toggle_optitrack_markers"}'); handleOptitrackMarkerToggle(context); break; case 'motion_recording_toggle': console.log('➡️ Routing to: MOTION RECORDING handler'); console.log(' Will send: {"type":"toggle_motion_recording"}'); handleMotionRecordingToggle(context); break; case 'optitrack_reconnect': console.log('➡️ Routing to: OPTITRACK RECONNECT handler'); console.log(' Will send: {"type":"reconnect_optitrack"}'); handleOptitrackReconnect(context); break; case 'facial_motion_reconnect': console.log('➡️ Routing to: FACIAL MOTION RECONNECT handler'); console.log(' Will send: {"type":"reconnect_facial_motion"}'); handleFacialMotionReconnect(context); break; case 'screenshot': console.log('➡️ Routing to: SCREENSHOT handler'); console.log(' Will send: {"type":"capture_screenshot"}'); handleScreenshot(context); break; case 'screenshot_alpha': console.log('➡️ Routing to: ALPHA SCREENSHOT handler'); console.log(' Will send: {"type":"capture_alpha_screenshot"}'); handleAlphaScreenshot(context); break; default: console.log('⚠️ WARNING: Unknown actionType:', actionType); console.log(' Defaulting to CAMERA handler'); handleCameraAction(settings); } } // 카메라 액션 처리 function handleCameraAction(settings) { let cameraIndex = settings.cameraIndex; // 카메라 인덱스가 설정되지 않았으면 0 사용 if (typeof cameraIndex !== 'number') { cameraIndex = 0; console.log('⚠️ 카메라 인덱스가 설정되지 않음, 기본값 0 사용'); } console.log('📹 전환할 카메라 인덱스:', cameraIndex); // Unity에 카메라 전환 요청 const message = JSON.stringify({ type: 'switch_camera', data: { camera_index: cameraIndex } }); console.log('📤 Unity에 카메라 전환 요청 전송:', message); console.log('🔍 Unity 연결 상태:', isUnityConnected); console.log('🔍 Unity 소켓 상태:', !!unitySocket); if (unitySocket && unitySocket.readyState === WebSocket.OPEN) { unitySocket.send(message); console.log('✅ 메시지 전송 완료'); } else { console.error('❌ Unity 소켓이 연결되지 않음'); console.log('🔄 Unity 재연결 시도...'); connectToUnity(); } } // 아이템 액션 처리 function handleItemAction(settings) { let itemIndex = settings.itemIndex; const itemAction = settings.itemAction || 'toggle'; // 기본값은 toggle // 아이템 인덱스가 설정되지 않았으면 0 사용 if (typeof itemIndex !== 'number') { itemIndex = 0; console.log('⚠️ 아이템 인덱스가 설정되지 않음, 기본값 0 사용'); } console.log('🎯 아이템 액션:', itemAction, '인덱스:', itemIndex); let messageType = 'toggle_item'; if (itemAction === 'set') { messageType = 'set_item'; } // Unity에 아이템 액션 요청 const message = JSON.stringify({ type: messageType, data: { item_index: itemIndex } }); console.log('📤 Unity에 아이템 액션 요청 전송:', message); console.log('🔍 Unity 연결 상태:', isUnityConnected); console.log('🔍 Unity 소켓 상태:', !!unitySocket); if (unitySocket && unitySocket.readyState === WebSocket.OPEN) { unitySocket.send(message); console.log('✅ 메시지 전송 완료'); } else { console.error('❌ Unity 소켓이 연결되지 않음'); console.log('🔄 Unity 재연결 시도...'); connectToUnity(); } } // 이벤트 액션 처리 function handleEventAction(settings) { let eventIndex = settings.eventIndex; const eventAction = settings.eventAction || 'execute'; // 기본값은 execute // 이벤트 인덱스가 설정되지 않았으면 0 사용 if (typeof eventIndex !== 'number') { eventIndex = 0; console.log('⚠️ 이벤트 인덱스가 설정되지 않음, 기본값 0 사용'); } console.log('🎯 이벤트 액션:', eventAction, '인덱스:', eventIndex); let messageType = 'execute_event'; if (eventAction === 'set') { messageType = 'set_event'; } // Unity에 이벤트 액션 요청 const message = JSON.stringify({ type: messageType, data: { event_index: eventIndex } }); console.log('📤 Unity에 이벤트 액션 요청 전송:', message); console.log('🔍 Unity 연결 상태:', isUnityConnected); console.log('🔍 Unity 소켓 상태:', !!unitySocket); if (unitySocket && unitySocket.readyState === WebSocket.OPEN) { unitySocket.send(message); console.log('✅ 메시지 전송 완료'); } else { console.error('❌ Unity 소켓이 연결되지 않음'); console.log('🔄 Unity 재연결 시도...'); connectToUnity(); } } // 아바타 의상 액션 처리 function handleAvatarOutfitAction(settings) { let avatarIndex = settings.avatarIndex; let outfitIndex = settings.outfitIndex; // 설정값들 검증 if (typeof avatarIndex !== 'number') { avatarIndex = 0; console.log('⚠️ 아바타 인덱스가 설정되지 않음, 기본값 0 사용'); } if (typeof outfitIndex !== 'number') { outfitIndex = 0; console.log('⚠️ 의상 인덱스가 설정되지 않음, 기본값 0 사용'); } console.log('👗 아바타 의상 전환:', { avatarIndex, outfitIndex }); // Unity에 아바타 의상 변경 요청 const message = JSON.stringify({ type: 'set_avatar_outfit', data: { avatar_index: avatarIndex, outfit_index: outfitIndex } }); console.log('📤 Unity에 아바타 의상 변경 요청 전송:', message); console.log('🔍 Unity 연결 상태:', isUnityConnected); console.log('🔍 Unity 소켓 상태:', !!unitySocket); if (unitySocket && unitySocket.readyState === WebSocket.OPEN) { unitySocket.send(message); console.log('✅ 메시지 전송 완료'); } else { console.error('❌ Unity 소켓이 연결되지 않음'); console.log('🔄 Unity 재연결 시도...'); connectToUnity(); } } // OptiTrack 마커 토글 액션 처리 function handleOptitrackMarkerToggle(context) { console.log('🎯 OptiTrack 마커 토글 실행'); // Unity에 마커 토글 요청 const message = JSON.stringify({ type: 'toggle_optitrack_markers' }); console.log('📤 Unity에 OptiTrack 마커 토글 요청 전송:', message); console.log('🔍 Unity 연결 상태:', isUnityConnected); console.log('🔍 Unity 소켓 상태:', !!unitySocket); if (unitySocket && unitySocket.readyState === WebSocket.OPEN) { unitySocket.send(message); console.log('✅ 메시지 전송 완료'); // 버튼 상태 토글 (0 <-> 1) toggleButtonState(context); } else { console.error('❌ Unity 소켓이 연결되지 않음'); console.log('🔄 Unity 재연결 시도...'); connectToUnity(); } } // 버튼 상태 토글 함수 function toggleButtonState(context) { const settings = getCurrentSettings(context); const currentState = settings.currentState || 0; const newState = currentState === 0 ? 1 : 0; // 설정 업데이트 settings.currentState = newState; setCurrentSettings(context, settings); // 버튼 상태 변경 const stateMessage = { event: 'setState', context: context, payload: { state: newState, target: 0 // hardware and software } }; if (websocket) { websocket.send(JSON.stringify(stateMessage)); console.log('🎨 버튼 상태 업데이트:', newState === 0 ? 'ON' : 'OFF'); } // 제목도 함께 변경 const title = newState === 0 ? '마커\nON' : '마커\nOFF'; setButtonTitle(context, title); } // 모션 녹화 토글 액션 처리 function handleMotionRecordingToggle(context) { console.log('🎬 모션 녹화 토글 실행'); // Unity에 모션 녹화 토글 요청 const message = JSON.stringify({ type: 'toggle_motion_recording' }); console.log('📤 Unity에 모션 녹화 토글 요청 전송:', message); console.log('🔍 Unity 연결 상태:', isUnityConnected); console.log('🔍 Unity 소켓 상태:', !!unitySocket); if (unitySocket && unitySocket.readyState === WebSocket.OPEN) { unitySocket.send(message); console.log('✅ 메시지 전송 완료'); // 버튼 상태 토글 (0 <-> 1) toggleMotionRecordingButtonState(context); } else { console.error('❌ Unity 소켓이 연결되지 않음'); console.log('🔄 Unity 재연결 시도...'); connectToUnity(); } } // 모션 녹화 버튼 상태 토글 함수 function toggleMotionRecordingButtonState(context) { const settings = getCurrentSettings(context); const currentState = settings.currentState || 0; const newState = currentState === 0 ? 1 : 0; // 설정 업데이트 settings.currentState = newState; setCurrentSettings(context, settings); // 버튼 상태 변경 const stateMessage = { event: 'setState', context: context, payload: { state: newState, target: 0 // hardware and software } }; if (websocket) { websocket.send(JSON.stringify(stateMessage)); console.log('🎨 녹화 버튼 상태 업데이트:', newState === 0 ? 'OFF' : 'REC'); } // 제목도 함께 변경 const title = newState === 0 ? '녹화\nOFF' : '녹화\nREC'; setButtonTitle(context, title); } // OptiTrack 재접속 액션 처리 function handleOptitrackReconnect(context) { console.log('🔄 OptiTrack 재접속 실행'); // Unity에 OptiTrack 재접속 요청 const message = JSON.stringify({ type: 'reconnect_optitrack' }); console.log('📤 Unity에 OptiTrack 재접속 요청 전송:', message); console.log('🔍 Unity 연결 상태:', isUnityConnected); console.log('🔍 Unity 소켓 상태:', !!unitySocket); if (unitySocket && unitySocket.readyState === WebSocket.OPEN) { unitySocket.send(message); console.log('✅ 메시지 전송 완료'); // 재접속은 상태가 없는 단순 액션이므로 버튼 상태 변경 없음 // 피드백을 위해 제목을 잠시 변경할 수 있음 setButtonTitle(context, '재접속\n중...'); // 1초 후 원래 제목으로 복구 setTimeout(() => { setButtonTitle(context, '옵티트랙\n재접속'); }, 1000); } else { console.error('❌ Unity 소켓이 연결되지 않음'); console.log('🔄 Unity 재연결 시도...'); connectToUnity(); } } // Facial Motion 재접속 액션 처리 function handleFacialMotionReconnect(context) { console.log('😊 Facial Motion 재접속 실행'); // Unity에 Facial Motion 재접속 요청 const message = JSON.stringify({ type: 'reconnect_facial_motion' }); console.log('📤 Unity에 Facial Motion 재접속 요청 전송:', message); console.log('🔍 Unity 연결 상태:', isUnityConnected); console.log('🔍 Unity 소켓 상태:', !!unitySocket); if (unitySocket && unitySocket.readyState === WebSocket.OPEN) { unitySocket.send(message); console.log('✅ 메시지 전송 완료'); // 재접속은 상태가 없는 단순 액션이므로 버튼 상태 변경 없음 // 피드백을 위해 제목을 잠시 변경할 수 있음 setButtonTitle(context, '재접속\n중...'); // 1초 후 원래 제목으로 복구 setTimeout(() => { setButtonTitle(context, '페이셜\n재접속'); }, 1000); } else { console.error('❌ Unity 소켓이 연결되지 않음'); console.log('🔄 Unity 재연결 시도...'); connectToUnity(); } } // 스크린샷 액션 처리 function handleScreenshot(context) { console.log('📸 스크린샷 촬영 실행'); // Unity에 스크린샷 요청 const message = JSON.stringify({ type: 'capture_screenshot' }); console.log('📤 Unity에 스크린샷 요청 전송:', message); console.log('🔍 Unity 연결 상태:', isUnityConnected); console.log('🔍 Unity 소켓 상태:', !!unitySocket); if (unitySocket && unitySocket.readyState === WebSocket.OPEN) { unitySocket.send(message); console.log('✅ 메시지 전송 완료'); // 피드백을 위해 제목을 잠시 변경 setButtonTitle(context, '촬영\n중...'); // 1초 후 원래 제목으로 복구 setTimeout(() => { setButtonTitle(context, '스크린샷'); }, 1000); } else { console.error('❌ Unity 소켓이 연결되지 않음'); console.log('🔄 Unity 재연결 시도...'); connectToUnity(); } } // 알파 스크린샷 액션 처리 function handleAlphaScreenshot(context) { console.log('📷 알파 스크린샷 촬영 실행'); // Unity에 알파 스크린샷 요청 const message = JSON.stringify({ type: 'capture_alpha_screenshot' }); console.log('📤 Unity에 알파 스크린샷 요청 전송:', message); console.log('🔍 Unity 연결 상태:', isUnityConnected); console.log('🔍 Unity 소켓 상태:', !!unitySocket); if (unitySocket && unitySocket.readyState === WebSocket.OPEN) { unitySocket.send(message); console.log('✅ 메시지 전송 완료'); // 피드백을 위해 제목을 잠시 변경 setButtonTitle(context, '촬영\n중...'); // 1초 후 원래 제목으로 복구 setTimeout(() => { setButtonTitle(context, '알파\n스크린샷'); }, 1000); } else { console.error('❌ Unity 소켓이 연결되지 않음'); console.log('🔄 Unity 재연결 시도...'); connectToUnity(); } } // Property Inspector 메시지 처리 function handlePropertyInspectorMessage(payload, context, actionUUID) { const command = payload.command; console.log('📤 Property Inspector 명령 처리 시작'); console.log(' - payload:', payload); console.log(' - context:', context); console.log(' - actionUUID:', actionUUID); console.log(' - command:', command); switch (command) { case 'get_unity_status': console.log('📡 Unity 상태 요청 - context:', context, 'action:', actionUUID); console.log('📡 Unity 연결 상태:', isUnityConnected); console.log('📡 카메라 목록 길이:', cameraList.length); console.log('📡 아이템 목록 길이:', itemList.length); console.log('📡 아바타 목록 길이:', avatarOutfitList.length); sendToPropertyInspector(context, 'unity_connected', { connected: isUnityConnected }, actionUUID); // Unity 연결된 상태라면 무조건 아바타 목록 요청 if (isUnityConnected && unitySocket && unitySocket.readyState === WebSocket.OPEN) { console.log('📡 Unity 연결됨 - 모든 데이터 요청 시작'); // 아바타 목록이 비어있으면 무조건 요청 if (avatarOutfitList.length === 0) { console.log('📡 아바타 목록 비어있음 - Unity에 요청'); const avatarMessage = JSON.stringify({ type: 'get_avatar_outfit_list' }); unitySocket.send(avatarMessage); console.log('📤 Unity에 아바타 의상 목록 강제 요청 전송'); } } if (isUnityConnected) { // actionUUID에 따라 해당하는 데이터만 전송 console.log('📡 ActionUUID에 따른 데이터 전송:', actionUUID); if (actionUUID === 'com.mirabox.streamingle.camera') { if (cameraList.length > 0) { // 현재 활성 카메라 인덱스 찾기 const currentCameraIndex = cameraList.findIndex(cam => cam.isActive) || 0; console.log('📡 현재 활성 카메라 인덱스:', currentCameraIndex); sendToPropertyInspector(context, 'camera_list', { cameras: cameraList, currentIndex: currentCameraIndex }, actionUUID); } } else if (actionUUID === 'com.mirabox.streamingle.item') { if (itemList.length > 0) { // 현재 활성 아이템 인덱스 찾기 const currentItemIndex = itemList.findIndex(item => item.isActive) || 0; console.log('📡 현재 활성 아이템 인덱스:', currentItemIndex); sendToPropertyInspector(context, 'item_list', { items: itemList, currentIndex: currentItemIndex }, actionUUID); } } else if (actionUUID === 'com.mirabox.streamingle.event') { if (eventList.length > 0) { // 현재 활성 이벤트 인덱스 찾기 const currentEventIndex = eventList.findIndex(event => event.isActive) || 0; console.log('📡 현재 활성 이벤트 인덱스:', currentEventIndex); sendToPropertyInspector(context, 'event_list', { events: eventList, currentIndex: currentEventIndex }, actionUUID); } } else if (actionUUID === 'com.mirabox.streamingle.avatar_outfit') { if (avatarOutfitList.length > 0) { // 현재 활성 아바타 인덱스 찾기 const currentAvatarIndex = avatarOutfitList.findIndex(avatar => avatar.current_outfit_index >= 0) || 0; console.log('📡 현재 아바타 목록:', avatarOutfitList.length, '개'); console.log('📡 현재 활성 아바타 인덱스:', currentAvatarIndex); sendToPropertyInspector(context, 'avatar_outfit_list', { avatars: avatarOutfitList, currentIndex: currentAvatarIndex }, actionUUID); } else { console.log('📡 아바타 목록이 비어있음, Unity에 요청'); // 아바타 목록이 없으면 Unity에 요청 if (unitySocket && unitySocket.readyState === WebSocket.OPEN) { const message = JSON.stringify({ type: 'get_avatar_outfit_list' }); unitySocket.send(message); console.log('📤 Unity에 아바타 의상 목록 자동 요청 전송'); } } } } else { // Unity가 연결되지 않은 경우에도 해당 컨트롤러에 맞는 빈 목록만 전송 console.log('📡 Unity 연결 안됨 - ActionUUID에 따른 빈 목록 전송:', actionUUID); if (actionUUID === 'com.mirabox.streamingle.camera') { sendToPropertyInspector(context, 'camera_list', { cameras: [], currentIndex: 0 }, actionUUID); } else if (actionUUID === 'com.mirabox.streamingle.item') { sendToPropertyInspector(context, 'item_list', { items: [], currentIndex: 0 }, actionUUID); } else if (actionUUID === 'com.mirabox.streamingle.event') { sendToPropertyInspector(context, 'event_list', { events: [], currentIndex: 0 }, actionUUID); } else if (actionUUID === 'com.mirabox.streamingle.avatar_outfit') { sendToPropertyInspector(context, 'avatar_outfit_list', { avatars: [], currentIndex: 0 }, actionUUID); } } break; case 'get_camera_list': console.log('📹 카메라 목록 요청'); if (isUnityConnected && unitySocket) { const message = JSON.stringify({ type: 'get_camera_list' }); unitySocket.send(message); console.log('📤 Unity에 카메라 목록 요청 전송'); } break; case 'get_item_list': console.log('🎯 아이템 목록 요청'); if (isUnityConnected && unitySocket) { const message = JSON.stringify({ type: 'get_item_list' }); unitySocket.send(message); console.log('📤 Unity에 아이템 목록 요청 전송'); } break; case 'get_event_list': console.log('🎯 이벤트 목록 요청'); if (isUnityConnected && unitySocket) { const message = JSON.stringify({ type: 'get_event_list' }); unitySocket.send(message); console.log('📤 Unity에 이벤트 목록 요청 전송'); } break; case 'switch_camera': console.log('📹 카메라 전환 요청:', payload.cameraIndex); if (isUnityConnected && unitySocket) { const message = JSON.stringify({ type: 'switch_camera', data: { camera_index: payload.cameraIndex } }); unitySocket.send(message); console.log('📤 Unity에 카메라 전환 요청 전송'); } break; case 'toggle_item': console.log('🎯 아이템 토글 요청:', payload.itemIndex); if (isUnityConnected && unitySocket) { const message = JSON.stringify({ type: 'toggle_item', data: { item_index: payload.itemIndex } }); unitySocket.send(message); console.log('📤 Unity에 아이템 토글 요청 전송'); } break; case 'set_item': console.log('🎯 아이템 설정 요청:', payload.itemIndex); if (isUnityConnected && unitySocket) { const message = JSON.stringify({ type: 'set_item', data: { item_index: payload.itemIndex } }); unitySocket.send(message); console.log('📤 Unity에 아이템 설정 요청 전송'); } break; case 'execute_event': console.log('🎯 이벤트 실행 요청:', payload.eventIndex); if (isUnityConnected && unitySocket) { const message = JSON.stringify({ type: 'execute_event', data: { event_index: payload.eventIndex } }); unitySocket.send(message); console.log('📤 Unity에 이벤트 실행 요청 전송'); } break; case 'set_event': console.log('🎯 이벤트 설정 요청:', payload.eventIndex); if (isUnityConnected && unitySocket) { const message = JSON.stringify({ type: 'set_event', data: { event_index: payload.eventIndex } }); unitySocket.send(message); console.log('📤 Unity에 이벤트 설정 요청 전송'); } break; case 'refresh_camera_list': console.log('🔄 카메라 목록 새로고침 요청'); if (isUnityConnected && unitySocket) { const message = JSON.stringify({ type: 'get_camera_list' }); unitySocket.send(message); console.log('📤 Unity에 카메라 목록 요청 전송'); } break; case 'refresh_item_list': console.log('🔄 아이템 목록 새로고침 요청'); if (isUnityConnected && unitySocket) { const message = JSON.stringify({ type: 'get_item_list' }); unitySocket.send(message); console.log('📤 Unity에 아이템 목록 요청 전송'); } break; case 'refresh_event_list': console.log('🔄 이벤트 목록 새로고침 요청'); if (isUnityConnected && unitySocket) { const message = JSON.stringify({ type: 'get_event_list' }); unitySocket.send(message); console.log('📤 Unity에 이벤트 목록 요청 전송'); } break; case 'refresh_avatar_outfit_list': console.log('🔄 아바타 의상 목록 새로고침 요청'); if (!isUnityConnected || !unitySocket) { console.log('🔄 Unity 연결되지 않음, 재연결 시도...'); connectToUnity(); } else { const message = JSON.stringify({ type: 'get_avatar_outfit_list' }); if (unitySocket && unitySocket.readyState === WebSocket.OPEN) { unitySocket.send(message); console.log('📤 Unity에 아바타 의상 목록 요청 전송'); } else { console.error('❌ Unity 소켓이 연결되지 않음'); console.log('🔄 Unity 재연결 시도...'); connectToUnity(); } } break; case 'set_avatar_outfit': console.log('👗 아바타 의상 설정 요청:', payload); if (isUnityConnected && unitySocket) { const message = JSON.stringify({ type: 'set_avatar_outfit', data: { avatar_index: payload.avatarIndex, outfit_index: payload.outfitIndex } }); unitySocket.send(message); console.log('📤 Unity에 아바타 의상 설정 요청 전송'); } break; case 'update_title': console.log('🏷️ 버튼 제목 업데이트 요청'); console.log('🏷️ 요청 payload:', payload); console.log('🏷️ 현재 Plugin의 avatarOutfitList:', avatarOutfitList); updateButtonTitle(context); break; case 'debug_test': console.log('🔍 디버그 테스트 메시지 수신!'); console.log(' - context:', context); console.log(' - actionUUID:', actionUUID); console.log(' - payload:', payload); console.log(' - 현재 등록된 컨텍스트들:', Array.from(buttonContexts.keys())); console.log(' - 이 컨텍스트의 설정:', getCurrentSettings(context)); // 응답을 보내서 Property Inspector가 받을 수 있는지 확인 sendToPropertyInspector(context, 'debug_response', { message: 'Plugin received debug test', receivedContext: context, registeredContexts: Array.from(buttonContexts.keys()) }, actionUUID); break; default: console.log('❓ 알 수 없는 Property Inspector 명령:', command); } } // Property Inspector로 메시지 전송 (action UUID 지정 가능) function sendToPropertyInspector(context, event, payload, actionUUID = null) { console.log('📤 Property Inspector로 메시지 전송 시작 - context:', context, 'event:', event, 'actionUUID:', actionUUID); if (!websocket || !context) { console.log('🚫 WebSocket 또는 context 없음 - Property Inspector 메시지 전송 건너뜀'); console.log('🔍 websocket 상태:', !!websocket); console.log('🔍 context:', context); return; } // action UUID 결정 let action = actionUUID; if (!action) { // Property Inspector에서 보내는 메시지의 action UUID를 그대로 사용 // 카메라 컨트롤러 Property Inspector는 'com.mirabox.streamingle.camera'를 사용 // 아이템 컨트롤러 Property Inspector는 'com.mirabox.streamingle.item'을 사용 action = 'com.mirabox.streamingle.camera'; // 기본값 // context별로 적절한 action 결정 const settings = getCurrentSettings(context); console.log('📤 context 설정:', settings); const actionType = settings.actionType || 'camera'; if (actionType === 'item') { action = 'com.mirabox.streamingle.item'; } else if (actionType === 'event') { action = 'com.mirabox.streamingle.event'; } else if (actionType === 'avatar_outfit') { action = 'com.mirabox.streamingle.avatar_outfit'; } } console.log('📤 결정된 action:', action); const message = { action: action, event: 'sendToPropertyInspector', context: context, payload: { event: event, ...payload } }; console.log('📤 Property Inspector로 전송할 메시지:', JSON.stringify(message, null, 2)); websocket.send(JSON.stringify(message)); console.log('✅ Property Inspector로 메시지 전송 완료:', event, payload); } // Unity 메시지 처리 function handleUnityMessage(data) { console.log('📨 Unity 메시지 수신:', data.type); console.log('📋 전체 메시지 데이터:', JSON.stringify(data, null, 2)); switch (data.type) { case 'connection_established': console.log('🎉 Unity 연결 확인됨'); console.log('📋 connection_established 데이터:', JSON.stringify(data, null, 2)); // 연결 시 카메라 및 아이템 데이터 저장 if (data.data) { // 카메라 데이터 처리 if (data.data.camera_data) { let cameras = data.data.camera_data.presets || data.data.camera_data; if (Array.isArray(cameras)) { cameraList = cameras; console.log('📹 카메라 목록 저장됨:', cameraList.length, '개'); } else { cameraList = []; console.log('⚠️ 카메라 데이터가 배열이 아님:', cameras); } } // 아이템 데이터 처리 if (data.data.item_data) { let items = data.data.item_data.items || data.data.item_data; if (Array.isArray(items)) { itemList = items; console.log('🎯 아이템 목록 저장됨:', itemList.length, '개'); } else { itemList = []; console.log('⚠️ 아이템 데이터가 배열이 아님:', items); } } // 이벤트 데이터 처리 if (data.data.event_data) { let events = data.data.event_data.events || data.data.event_data; if (Array.isArray(events)) { eventList = events; console.log('🎯 이벤트 목록 저장됨:', eventList.length, '개'); } else { eventList = []; console.log('⚠️ 이벤트 데이터가 배열이 아님:', events); } } // 아바타 의상 데이터 처리 if (data.data.avatar_outfit_data) { let avatars = data.data.avatar_outfit_data.avatars || data.data.avatar_outfit_data; if (Array.isArray(avatars)) { avatarOutfitList = avatars; console.log('👗 아바타 의상 목록 저장됨:', avatarOutfitList.length, '개'); } else { avatarOutfitList = []; console.log('⚠️ 아바타 의상 데이터가 배열이 아님:', avatars); } } else { // 아바타 의상 데이터가 없어도 빈 배열로 초기화 console.log('⚠️ 아바타 의상 데이터가 없음, 빈 배열로 초기화'); avatarOutfitList = []; } updateAllButtonTitles(); // Property Inspector들에게 Unity 연결 상태 알림 console.log('📤 Property Inspector들에게 Unity 연결 알림 전송 시작'); console.log('📤 현재 등록된 컨텍스트들:', Array.from(buttonContexts.keys())); if (buttonContexts.size === 0) { console.log('⚠️ 등록된 컨텍스트가 없음 - Property Inspector가 아직 연결되지 않았을 수 있음'); } else { console.log('📤 등록된 컨텍스트들:', Array.from(buttonContexts.keys())); for (const context of buttonContexts.keys()) { console.log('📤 Property Inspector로 전송할 컨텍스트:', context); // context별로 적절한 action UUID 결정 const settings = getCurrentSettings(context); const actionType = settings.actionType || 'camera'; let actionUUID = 'com.mirabox.streamingle.camera'; if (actionType === 'item') { actionUUID = 'com.mirabox.streamingle.item'; } else if (actionType === 'event') { actionUUID = 'com.mirabox.streamingle.event'; } else if (actionType === 'avatar_outfit') { actionUUID = 'com.mirabox.streamingle.avatar_outfit'; } console.log('🔍 컨텍스트 분석:', context, 'Action Type:', actionType, 'Action UUID:', actionUUID); sendToPropertyInspector(context, 'unity_connected', { connected: true }, actionUUID); // action UUID에 따라 적절한 데이터만 전송 if (actionUUID === 'com.mirabox.streamingle.camera') { // 카메라 컨트롤러에는 카메라 데이터만 전송 let currentCameraIndex = 0; if (typeof data.data.camera_data?.current_index === 'number' && data.data.camera_data.current_index >= 0) { currentCameraIndex = data.data.camera_data.current_index; } console.log('📹 카메라 컨트롤러에 카메라 데이터 전송:', context, '카메라 수:', cameraList.length); sendToPropertyInspector(context, 'camera_list', { cameras: cameraList, currentIndex: currentCameraIndex }, actionUUID); } else if (actionUUID === 'com.mirabox.streamingle.item') { // 아이템 컨트롤러에는 아이템 데이터만 전송 let currentItemIndex = 0; if (typeof data.data.item_data?.current_index === 'number' && data.data.item_data.current_index >= 0) { currentItemIndex = data.data.item_data.current_index; } console.log('🎯 아이템 컨트롤러에 아이템 데이터 전송:', context, '아이템 수:', itemList.length); sendToPropertyInspector(context, 'item_list', { items: itemList, currentIndex: currentItemIndex }, actionUUID); } else if (actionUUID === 'com.mirabox.streamingle.event') { // 이벤트 컨트롤러에는 이벤트 데이터만 전송 let currentEventIndex = 0; if (typeof data.data.event_data?.current_index === 'number' && data.data.event_data.current_index >= 0) { currentEventIndex = data.data.event_data.current_index; } console.log('🎯 이벤트 컨트롤러에 이벤트 데이터 전송:', context, '이벤트 수:', eventList.length); sendToPropertyInspector(context, 'event_list', { events: eventList, currentIndex: currentEventIndex }, actionUUID); } else if (actionUUID === 'com.mirabox.streamingle.avatar_outfit') { // 아바타 의상 컨트롤러에는 아바타 의상 데이터만 전송 let currentAvatarIndex = 0; if (typeof data.data.avatar_outfit_data?.current_avatar_index === 'number' && data.data.avatar_outfit_data.current_avatar_index >= 0) { currentAvatarIndex = data.data.avatar_outfit_data.current_avatar_index; } console.log('👗 아바타 의상 컨트롤러에 아바타 데이터 전송:', context, '아바타 수:', avatarOutfitList.length); // 아바타 목록이 비어있더라도 연결 상태와 함께 전송 sendToPropertyInspector(context, 'avatar_outfit_list', { avatars: avatarOutfitList || [], currentIndex: currentAvatarIndex }, actionUUID); } } console.log('✅ Property Inspector들에게 Unity 연결 알림 전송 완료'); } } else { console.log('⚠️ Unity 연결 시 데이터가 없음'); console.log('📋 data.data:', data.data); } break; case 'camera_list_response': console.log('📹 카메라 목록 수신'); if (data.data && data.data.camera_data) { let cameras = data.data.camera_data.presets || data.data.camera_data; if (Array.isArray(cameras)) { cameraList = cameras; const currentIndex = data.data.current_camera ?? data.data.camera_data?.current_index ?? 0; console.log('📹 카메라 목록 업데이트됨:', cameraList.length, '개'); updateAllButtonTitles(); // Property Inspector들에게 카메라 목록 전송 (카메라 컨트롤러만) for (const context of buttonContexts.keys()) { const settings = getCurrentSettings(context); const actionType = settings.actionType || 'camera'; if (actionType === 'camera') { sendToPropertyInspector(context, 'camera_list', { cameras: cameraList, currentIndex: currentIndex }, 'com.mirabox.streamingle.camera'); // 카메라 상태에 따라 버튼 상태도 업데이트 const cameraIndex = settings.cameraIndex || 0; const isActive = (cameraIndex === currentIndex); setButtonState(context, isActive); console.log('🎨 카메라 변경으로 상태 업데이트:', context, '카메라 인덱스:', cameraIndex, '활성:', isActive); } } } else { console.log('⚠️ 카메라 목록 응답에서 카메라 데이터가 배열이 아님'); console.log('📋 cameras:', cameras); } } break; case 'camera_changed': console.log('📹 카메라 변경 알림'); if (data.data && data.data.camera_data) { let cameras = data.data.camera_data.presets || data.data.camera_data; if (Array.isArray(cameras)) { cameraList = cameras; console.log('📹 카메라 목록 업데이트됨:', cameraList.length, '개'); updateAllButtonTitles(); // Property Inspector들에게 카메라 목록 전송 (카메라 컨트롤러만) for (const context of buttonContexts.keys()) { const settings = getCurrentSettings(context); const actionType = settings.actionType || 'camera'; if (actionType === 'camera') { sendToPropertyInspector(context, 'camera_list', { cameras: cameraList, currentIndex: data.data.camera_data?.current_index || 0 }, 'com.mirabox.streamingle.camera'); // 카메라 상태에 따라 버튼 상태도 업데이트 const cameraIndex = settings.cameraIndex || 0; let isActive = false; if (cameraList && cameraList[cameraIndex]) { isActive = cameraList[cameraIndex].isActive === true; } setButtonState(context, isActive); console.log('🎨 카메라 변경으로 상태 업데이트:', context, '카메라 인덱스:', cameraIndex, 'isActive:', isActive); } } } else { console.log('⚠️ 카메라 변경 응답에서 카메라 데이터가 배열이 아님'); console.log('📋 cameras:', cameras); } } break; case 'item_changed': console.log('🎯 아이템 변경 알림'); if (data.data && data.data.item_data) { let items = data.data.item_data.items || data.data.item_data; if (Array.isArray(items)) { itemList = items; console.log('🎯 아이템 목록 업데이트됨:', itemList.length, '개'); updateAllButtonTitles(); // Property Inspector들에게 아이템 목록 전송 (아이템 컨트롤러만) for (const context of buttonContexts.keys()) { const settings = getCurrentSettings(context); const actionType = settings.actionType || 'camera'; if (actionType === 'item') { sendToPropertyInspector(context, 'item_list', { items: itemList, currentIndex: data.data.item_data?.current_index || 0 }, 'com.mirabox.streamingle.item'); // 아이템 상태에 따라 버튼 상태도 업데이트 const itemIndex = settings.itemIndex || 0; if (itemList[itemIndex]) { const isActive = itemList[itemIndex].isActive !== false; // 기본값은 true setButtonState(context, isActive); console.log('🎨 아이템 변경으로 상태 업데이트:', context, '아이템 인덱스:', itemIndex, '활성:', isActive); } } } } else { console.log('⚠️ 아이템 목록 응답에서 아이템 데이터가 배열이 아님'); console.log('📋 items:', items); } } break; case 'item_list_response': console.log('🎯 아이템 목록 수신'); if (data.data && data.data.item_data) { let items = data.data.item_data.items || data.data.item_data; if (Array.isArray(items)) { itemList = items; const currentIndex = data.data.current_item ?? data.data.item_data?.current_index ?? 0; console.log('🎯 아이템 목록 업데이트됨:', itemList.length, '개'); updateAllButtonTitles(); // Property Inspector들에게 아이템 목록 전송 (아이템 컨트롤러만) for (const context of buttonContexts.keys()) { const settings = getCurrentSettings(context); const actionType = settings.actionType || 'camera'; if (actionType === 'item') { sendToPropertyInspector(context, 'item_list', { items: itemList, currentIndex: currentIndex }, 'com.mirabox.streamingle.item'); // 아이템 상태에 따라 버튼 상태도 업데이트 const itemIndex = settings.itemIndex || 0; if (itemList[itemIndex]) { const isActive = itemList[itemIndex].isActive !== false; // 기본값은 true setButtonState(context, isActive); } } } } else { console.log('⚠️ 아이템 목록 응답에서 아이템 데이터가 배열이 아님'); console.log('📋 items:', items); } } break; case 'event_changed': console.log('🎯 이벤트 변경 알림'); console.log('📋 이벤트 데이터 구조:', JSON.stringify(data.data, null, 2)); if (data.data && data.data.event_data) { let events = data.data.event_data.events || data.data.event_data; console.log('🎯 추출된 이벤트 배열:', events); console.log('🎯 이벤트 배열 타입:', Array.isArray(events) ? 'Array' : typeof events); if (Array.isArray(events)) { eventList = events; console.log('🎯 이벤트 목록 업데이트됨:', eventList.length, '개'); console.log('🎯 이벤트 목록 내용:', JSON.stringify(eventList, null, 2)); updateAllButtonTitles(); // Property Inspector들에게 이벤트 목록 전송 (이벤트 컨트롤러만) for (const context of buttonContexts.keys()) { const settings = getCurrentSettings(context); const actionType = settings.actionType || 'camera'; console.log('🔍 컨텍스트 분석:', context, 'Action Type:', actionType); if (actionType === 'event') { console.log('🎯 이벤트 컨트롤러 발견 - Property Inspector에 데이터 전송'); sendToPropertyInspector(context, 'event_list', { events: eventList, currentIndex: data.data.event_data?.current_index || 0 }, 'com.mirabox.streamingle.event'); } else { console.log('⚠️ 이벤트 컨트롤러가 아님 - Action Type:', actionType); } } } else { console.log('⚠️ 이벤트 목록 응답에서 이벤트 데이터가 배열이 아님'); console.log('📋 events:', events); } } break; case 'event_list_response': console.log('🎯 이벤트 목록 수신'); if (data.data && data.data.event_data) { let events = data.data.event_data.events || data.data.event_data; if (Array.isArray(events)) { eventList = events; const currentIndex = data.data.current_event ?? data.data.event_data?.current_index ?? 0; console.log('🎯 이벤트 목록 업데이트됨:', eventList.length, '개'); updateAllButtonTitles(); // Property Inspector들에게 이벤트 목록 전송 (이벤트 컨트롤러만) for (const context of buttonContexts.keys()) { const settings = getCurrentSettings(context); const actionType = settings.actionType || 'camera'; if (actionType === 'event') { sendToPropertyInspector(context, 'event_list', { events: eventList, currentIndex: currentIndex }, 'com.mirabox.streamingle.event'); } } } else { console.log('⚠️ 이벤트 목록 응답에서 이벤트 데이터가 배열이 아님'); console.log('📋 events:', events); } } break; case 'avatar_outfit_changed': console.log('👗 아바타 의상 변경 알림'); if (data.data && data.data.avatar_outfit_data) { let avatars = data.data.avatar_outfit_data.avatars || data.data.avatar_outfit_data; if (Array.isArray(avatars)) { avatarOutfitList = avatars; console.log('👗 아바타 의상 목록 업데이트됨:', avatarOutfitList.length, '개'); updateAllButtonTitles(); // Property Inspector들에게 아바타 의상 목록 전송 (아바타 의상 컨트롤러만) for (const context of buttonContexts.keys()) { const settings = getCurrentSettings(context); const actionType = settings.actionType || 'camera'; if (actionType === 'avatar_outfit') { sendToPropertyInspector(context, 'avatar_outfit_changed', { avatars: avatarOutfitList, currentIndex: data.data.avatar_outfit_data?.current_avatar_index || 0 }, 'com.mirabox.streamingle.avatar_outfit'); } } } else { console.log('⚠️ 아바타 의상 변경 응답에서 아바타 데이터가 배열이 아님'); console.log('📋 avatars:', avatars); } } break; case 'avatar_outfit_list_response': console.log('👗 아바타 의상 목록 응답'); if (data.data && data.data.avatar_outfit_data) { let avatars = data.data.avatar_outfit_data.avatars || data.data.avatar_outfit_data; let currentIndex = data.data.avatar_outfit_data?.current_avatar_index || 0; if (Array.isArray(avatars)) { avatarOutfitList = avatars; console.log('👗 아바타 의상 목록 응답 처리됨:', avatarOutfitList.length, '개'); // 아바타 데이터 상세 로그 avatarOutfitList.forEach((avatar, index) => { console.log(`👗 아바타 ${index}:`, { name: avatar.name, nameType: typeof avatar.name, current_outfit_name: avatar.current_outfit_name, outfits: avatar.outfits ? avatar.outfits.length : 'no outfits' }); }); updateAllButtonTitles(); // Property Inspector들에게 아바타 의상 목록 전송 for (const context of buttonContexts.keys()) { const settings = getCurrentSettings(context); const actionType = settings.actionType || 'camera'; if (actionType === 'avatar_outfit') { sendToPropertyInspector(context, 'avatar_outfit_list', { avatars: avatarOutfitList, currentIndex: currentIndex }, 'com.mirabox.streamingle.avatar_outfit'); } } } else { console.log('⚠️ 아바타 의상 목록 응답에서 아바타 데이터가 배열이 아님'); console.log('📋 avatars:', avatars); } } break; default: console.log('❓ 알 수 없는 Unity 메시지 타입:', data.type); } } // 버튼 제목을 직접 설정하는 함수 function setButtonTitle(context, title) { if (!websocket || !context) { console.log('🚫 WebSocket 또는 context 없음 - 제목 설정 건너뜀'); return; } console.log(`🏷️ 버튼 제목 설정: "${title}" (Context: ${context})`); const message = { event: 'setTitle', context: context, payload: { title: title, target: 0 // hardware and software } }; websocket.send(JSON.stringify(message)); console.log('✅ setTitle 메시지 전송 완료'); } // 모든 버튼의 제목 업데이트 function updateAllButtonTitles() { for (const context of buttonContexts.keys()) { updateButtonTitle(context); } } // StreamDock 버튼 제목 업데이트 function updateButtonTitle(context) { if (!websocket || !context) { console.log('🚫 WebSocket 또는 context 없음 - 제목 업데이트 건너뜀'); return; } const settings = getCurrentSettings(context); const actionType = settings.actionType || 'camera'; let title = 'Camera'; let isActive = true; // 아이템 활성화 상태 (기본값: 활성) console.log('🏷️ 버튼 제목 업데이트 시작 - Context:', context); console.log('🏷️ 설정:', settings); console.log('🏷️ 액션 타입:', actionType); if (actionType === 'camera') { const cameraIndex = typeof settings.cameraIndex === 'number' ? settings.cameraIndex : 0; title = `카메라 ${cameraIndex + 1}`; // 카메라 목록에서 이름 찾기 if (cameraList && cameraList.length > cameraIndex) { const camera = cameraList[cameraIndex]; if (camera && camera.name) { // 카메라 이름에서 불필요한 부분 제거하고 짧게 만들기 let shortName = camera.name .replace('Cam0', '') .replace('Cam', '') .replace('_', ' ') .substring(0, 10); // 최대 10글자 title = shortName || `카메라 ${cameraIndex + 1}`; } // 카메라 활성화 상태 확인 if (typeof camera.isActive === 'boolean') { isActive = camera.isActive; } } } else if (actionType === 'item') { const itemIndex = typeof settings.itemIndex === 'number' ? settings.itemIndex : 0; title = `아이템 ${itemIndex + 1}`; // 아이템 목록에서 이름 찾기 if (itemList && itemList.length > itemIndex) { const item = itemList[itemIndex]; if (item && (item.name || item.groupName)) { // 아이템 이름에서 불필요한 부분 제거하고 짧게 만들기 let itemName = item.name || item.groupName; let shortName = itemName .replace('Item', '') .replace('Group', '') .replace('_', ' ') .substring(0, 10); // 최대 10글자 title = shortName || `아이템 ${itemIndex + 1}`; // 아이템 활성화 상태 확인 if (typeof item.isActive === 'boolean') { isActive = item.isActive; } } } } else if (actionType === 'event') { const eventIndex = typeof settings.eventIndex === 'number' ? settings.eventIndex : 0; title = `이벤트 ${eventIndex + 1}`; // 이벤트 목록에서 이름 찾기 if (eventList && eventList.length > eventIndex) { const event = eventList[eventIndex]; if (event && (event.name || event.groupName)) { // 이벤트 이름에서 불필요한 부분 제거하고 짧게 만들기 let eventName = event.name || event.groupName; let shortName = eventName .replace('Event', '') .replace('Group', '') .replace('_', ' ') .substring(0, 10); // 최대 10글자 title = shortName || `이벤트 ${eventIndex + 1}`; } } } else if (actionType === 'avatar_outfit') { const avatarIndex = typeof settings.avatarIndex === 'number' ? settings.avatarIndex : 0; const outfitIndex = typeof settings.outfitIndex === 'number' ? settings.outfitIndex : 0; title = `아바타 ${avatarIndex + 1}`; console.log('👗 아바타 제목 업데이트 시작'); console.log('👗 Context:', context); console.log('👗 현재 설정:', settings); console.log('👗 아바타 인덱스:', avatarIndex, '의상 인덱스:', outfitIndex); console.log('👗 Plugin의 avatarOutfitList:', avatarOutfitList); console.log('👗 avatarOutfitList 길이:', avatarOutfitList ? avatarOutfitList.length : 'null'); // 버튼별 설정 확인 console.log('👗 [DEBUG] 모든 버튼 컨텍스트와 설정:'); for (const [ctx, ctxSettings] of buttonContexts.entries()) { if (ctxSettings.actionType === 'avatar_outfit') { console.log(`👗 [DEBUG] Context: ${ctx} -> avatarIndex: ${ctxSettings.avatarIndex}, outfitIndex: ${ctxSettings.outfitIndex}`); } } if (avatarOutfitList && avatarOutfitList.length > avatarIndex) { const avatar = avatarOutfitList[avatarIndex]; console.log('👗 선택된 아바타:', avatar); if (avatar && avatar.name) { console.log('👗 원본 아바타 이름:', avatar.name); console.log('👗 아바타 이름 타입:', typeof avatar.name); // 아바타 이름 안전하게 처리 let avatarName = String(avatar.name); console.log('👗 문자열 변환된 이름:', avatarName); // 아바타 이름 짧게 만들기 let shortName = avatarName .replace('Avatar', '') .replace('Character', '') .replace('_', ' ') .substring(0, 8); // 최대 8글자 console.log('👗 처리된 아바타 이름:', shortName); // 버튼에 설정된 의상 정보 추가 let outfitInfo = ''; if (avatar.outfits && avatar.outfits.length > outfitIndex) { const outfit = avatar.outfits[outfitIndex]; console.log('👗 버튼에 설정된 의상:', outfit); if (outfit && outfit.name) { outfitInfo = '\n' + String(outfit.name).substring(0, 6); console.log('👗 설정된 의상 이름:', outfit.name); } } else { console.log('👗 의상 인덱스가 범위를 벗어남 또는 의상 목록이 없음'); console.log('👗 outfitIndex:', outfitIndex, 'outfits 길이:', avatar.outfits ? avatar.outfits.length : 'null'); } title = (shortName || `아바타${avatarIndex + 1}`) + outfitInfo; console.log('👗 최종 제목:', title); // 아바타 의상 활성화 상태 확인 if (avatar.outfits && avatar.outfits.length > outfitIndex) { const outfit = avatar.outfits[outfitIndex]; // 현재 활성화된 의상이 설정된 의상과 같으면 활성 상태 isActive = (avatar.current_outfit_index === outfitIndex); console.log('👗 아바타 의상 활성 상태:', isActive, 'current:', avatar.current_outfit_index, 'target:', outfitIndex); } else { isActive = false; console.log('👗 의상을 찾을 수 없어 비활성 상태로 설정'); } } else { console.log('👗 아바타 이름이 없음:', avatar); isActive = false; } } else { console.log('👗 아바타 목록이 없거나 인덱스가 범위를 벗어남'); console.log('👗 목록 길이:', avatarOutfitList ? avatarOutfitList.length : 'null'); } } else if (actionType === 'optitrack_marker_toggle') { // OptiTrack 마커 토글 버튼 const currentState = settings.currentState || 0; title = currentState === 0 ? '마커\nON' : '마커\nOFF'; isActive = true; // 항상 활성 상태 console.log('🎯 OptiTrack 마커 버튼 제목:', title, '(State:', currentState, ')'); } else if (actionType === 'motion_recording_toggle') { // 모션 녹화 토글 버튼 const currentState = settings.currentState || 0; title = currentState === 0 ? '녹화\nOFF' : '녹화\nREC'; isActive = true; // 항상 활성 상태 console.log('🎬 모션 녹화 버튼 제목:', title, '(State:', currentState, ')'); } else if (actionType === 'optitrack_reconnect') { // OptiTrack 재접속 버튼 title = '옵티트랙\n재접속'; isActive = true; // 항상 활성 상태 console.log('🔄 OptiTrack 재접속 버튼 제목:', title); } else if (actionType === 'facial_motion_reconnect') { // Facial Motion 재접속 버튼 title = '페이셜\n재접속'; isActive = true; // 항상 활성 상태 console.log('😊 Facial Motion 재접속 버튼 제목:', title); } else if (actionType === 'screenshot') { // 스크린샷 버튼 title = '스크린샷'; isActive = true; // 항상 활성 상태 console.log('📸 스크린샷 버튼 제목:', title); } else if (actionType === 'screenshot_alpha') { // 알파 스크린샷 버튼 title = '알파\n스크린샷'; isActive = true; // 항상 활성 상태 console.log('📷 알파 스크린샷 버튼 제목:', title); } // StreamDock에 제목 업데이트 요청 const message = { event: 'setTitle', context: context, payload: { title: title, target: 0, // hardware and software titleParameters: { fontSize: 36, // 기존 24 → 36으로 키움 showTitle: true, titleAlignment: "bottom" // 중앙(middle) → 아래(bottom)으로 변경 } } }; websocket.send(JSON.stringify(message)); console.log('🏷️ 버튼 제목 업데이트:', title, '(액션 타입:', actionType, ', 활성:', isActive, ')'); // 아이템, 카메라, 아바타 의상이 비활성화되어 있으면 아이콘을 어둡게 표시 (이벤트는 제외) if ((actionType === 'item' || actionType === 'camera' || actionType === 'avatar_outfit') && !isActive) { setButtonState(context, false); // 비활성 상태로 설정 } else if (actionType === 'item' || actionType === 'camera' || actionType === 'avatar_outfit') { setButtonState(context, true); // 활성 상태로 설정 } // 이벤트 컨트롤러는 활성/비활성 상태 변경 없음 (항상 활성) } // 버튼 상태 설정 (활성/비활성) function setButtonState(context, isActive) { if (!websocket || !context) { console.log('🚫 WebSocket 또는 context 없음 - 버튼 상태 업데이트 건너뜀'); return; } const settings = getCurrentSettings(context); const actionType = settings.actionType || 'camera'; // 아이템, 카메라, 아바타 의상 컨트롤러만 상태 변경 적용 (이벤트 컨트롤러는 제외) if (actionType === 'item' || actionType === 'camera' || actionType === 'avatar_outfit') { // 방법 1: setState 이벤트 사용 const stateMessage = { event: 'setState', context: context, payload: { state: isActive ? 0 : 1, // 0: 활성 상태, 1: 비활성 상태 target: 0 // hardware and software } }; websocket.send(JSON.stringify(stateMessage)); console.log('🎨 버튼 상태 업데이트 (setState):', context, '(활성:', isActive, ', 상태:', isActive ? 0 : 1, ')'); console.log('🎨 버튼 상태 업데이트 완료:', context, '(활성:', isActive, ')'); } } // 설정 관리 헬퍼 함수들 function getCurrentSettings(context) { if (!context) return {}; return buttonContexts.get(context) || {}; } function setCurrentSettings(context, newSettings) { if (!context) return; buttonContexts.set(context, newSettings); } function updateCurrentSettings(context, partialSettings) { if (!context) return; const currentSettings = getCurrentSettings(context); setCurrentSettings(context, { ...currentSettings, ...partialSettings }); } // 브라우저 환경에서 자동 시작 console.log('🌐 브라우저 기반 플러그인 준비 완료');