diff --git a/Assets/Scripts/Streamdeck/StreamDeckServerManager.cs b/Assets/Scripts/Streamdeck/StreamDeckServerManager.cs index 5fa6be15..2711d2fa 100644 --- a/Assets/Scripts/Streamdeck/StreamDeckServerManager.cs +++ b/Assets/Scripts/Streamdeck/StreamDeckServerManager.cs @@ -9,24 +9,24 @@ public class StreamDeckServerManager : MonoBehaviour { [Header("서버 설정")] public int port = 10701; - + private WebSocketServer server; private List connectedClients = new List(); public CameraManager cameraManager { get; private set; } public ItemController itemController { get; private set; } - + // 싱글톤 패턴으로 StreamDeckService에서 접근 가능하도록 public static StreamDeckServerManager Instance { get; private set; } - + // 메인 스레드에서 처리할 작업 큐 private readonly Queue mainThreadActions = new Queue(); private readonly object lockObject = new object(); - + void Awake() { Instance = this; } - + void Start() { // CameraManager 찾기 @@ -36,17 +36,17 @@ public class StreamDeckServerManager : MonoBehaviour Debug.LogError("[StreamDeckServerManager] CameraManager를 찾을 수 없습니다!"); return; } - + // ItemController 찾기 itemController = FindObjectOfType(); if (itemController == null) { Debug.LogWarning("[StreamDeckServerManager] ItemController를 찾을 수 없습니다. 아이템 컨트롤 기능이 비활성화됩니다."); } - + StartServer(); } - + void Update() { // 메인 스레드에서 대기 중인 작업들을 처리 @@ -66,12 +66,12 @@ public class StreamDeckServerManager : MonoBehaviour } } } - + void OnApplicationQuit() { StopServer(); } - + private void StartServer() { try @@ -86,7 +86,7 @@ public class StreamDeckServerManager : MonoBehaviour Debug.LogError($"[StreamDeckServerManager] 서버 시작 실패: {e.Message}"); } } - + private void StopServer() { if (server != null) @@ -96,26 +96,28 @@ public class StreamDeckServerManager : MonoBehaviour Debug.Log("[StreamDeckServerManager] 서버 중지됨"); } } - + public void OnClientConnected(StreamDeckService service) { // 메인 스레드에서 처리하도록 큐에 추가 lock (lockObject) { - mainThreadActions.Enqueue(() => { + mainThreadActions.Enqueue(() => + { connectedClients.Add(service); Debug.Log($"[StreamDeckServerManager] 클라이언트 연결됨. 총 연결: {connectedClients.Count}"); SendInitialCameraData(service); }); } } - + // 메시지 처리도 메인 스레드로 전달 public void ProcessMessageOnMainThread(string messageData, StreamDeckService service) { lock (lockObject) { - mainThreadActions.Enqueue(() => { + mainThreadActions.Enqueue(() => + { try { ProcessMessage(messageData, service); @@ -127,17 +129,17 @@ public class StreamDeckServerManager : MonoBehaviour }); } } - + public void OnClientDisconnected(StreamDeckService service) { connectedClients.Remove(service); Debug.Log($"[StreamDeckServerManager] 클라이언트 연결 해제됨. 총 연결: {connectedClients.Count}"); } - + private void SendInitialCameraData(StreamDeckService service) { if (cameraManager == null) return; - + var initialData = new { type = "connection_established", @@ -153,12 +155,12 @@ public class StreamDeckServerManager : MonoBehaviour current_item = itemController?.GetCurrentItemState() } }; - + string json = JsonConvert.SerializeObject(initialData); service.SendMessage(json); Debug.Log("[StreamDeckServerManager] 초기 데이터 전송됨 (카메라 + 아이템)"); } - + public void BroadcastMessage(string message) { foreach (var client in connectedClients.ToArray()) @@ -174,12 +176,12 @@ public class StreamDeckServerManager : MonoBehaviour } } } - + // 카메라 변경 시 모든 클라이언트에게 알림 public void NotifyCameraChanged() { if (cameraManager == null) return; - + var updateMessage = new { type = "camera_changed", @@ -191,17 +193,17 @@ public class StreamDeckServerManager : MonoBehaviour current_camera = cameraManager.GetCurrentCameraState() } }; - + string json = JsonConvert.SerializeObject(updateMessage); BroadcastMessage(json); Debug.Log("[StreamDeckServerManager] 카메라 변경 알림 전송됨"); } - + // 아이템 변경 시 모든 클라이언트에게 알림 public void NotifyItemChanged() { if (itemController == null) return; - + var updateMessage = new { type = "item_changed", @@ -213,41 +215,41 @@ public class StreamDeckServerManager : MonoBehaviour current_item = itemController.GetCurrentItemState() } }; - + string json = JsonConvert.SerializeObject(updateMessage); BroadcastMessage(json); Debug.Log("[StreamDeckServerManager] 아이템 변경 알림 전송됨"); } - + // 메시지 처리 로직을 여기로 이동 private void ProcessMessage(string messageData, StreamDeckService service) { // JSON 파싱 시도 var message = JsonConvert.DeserializeObject>(messageData); string messageType = message.ContainsKey("type") ? message["type"].ToString() : null; - + switch (messageType) { case "switch_camera": HandleSwitchCamera(message); break; - + case "get_camera_list": HandleGetCameraList(service); break; - + case "toggle_item": HandleToggleItem(message); break; - + case "set_item": HandleSetItem(message); break; - + case "get_item_list": HandleGetItemList(service); break; - + case "test": // 테스트 메시지 에코 응답 var response = new @@ -259,48 +261,45 @@ public class StreamDeckServerManager : MonoBehaviour received_message = messageData } }; - + string json = JsonConvert.SerializeObject(response); service.SendMessage(json); break; - + default: Debug.Log($"[StreamDeckServerManager] 알 수 없는 메시지 타입: {messageType}"); break; } } - + private void HandleSwitchCamera(Dictionary message) { Debug.Log($"[StreamDeckServerManager] 카메라 전환 요청 수신"); - + if (cameraManager == null) { Debug.LogError("[StreamDeckServerManager] cameraManager가 null입니다!"); return; } - + try { if (message.ContainsKey("data")) { var dataObject = message["data"]; - + if (dataObject is Newtonsoft.Json.Linq.JObject jObject) - { + { if (jObject.ContainsKey("camera_index")) { var cameraIndexToken = jObject["camera_index"]; - + if (int.TryParse(cameraIndexToken?.ToString(), out int cameraIndex)) - { + { if (cameraIndex >= 0 && cameraIndex < (cameraManager.cameraPresets?.Count ?? 0)) - { + { Debug.Log($"[StreamDeckServerManager] 카메라 {cameraIndex}번으로 전환"); - cameraManager.Set(cameraIndex); - - // 카메라 변경 알림 전송 - NotifyCameraChanged(); + cameraManager.Set(cameraIndex); } else { @@ -322,17 +321,14 @@ public class StreamDeckServerManager : MonoBehaviour if (data.ContainsKey("camera_index")) { var cameraIndexObj = data["camera_index"]; - + if (int.TryParse(cameraIndexObj?.ToString(), out int cameraIndex)) { if (cameraIndex >= 0 && cameraIndex < (cameraManager.cameraPresets?.Count ?? 0)) { Debug.Log($"[StreamDeckServerManager] 카메라 {cameraIndex}번으로 전환"); cameraManager.Set(cameraIndex); - - // 카메라 변경 알림 전송 - NotifyCameraChanged(); - } + } else { Debug.LogError($"[StreamDeckServerManager] 잘못된 카메라 인덱스: {cameraIndex}, 유효 범위: 0-{(cameraManager.cameraPresets?.Count ?? 0) - 1}"); @@ -363,11 +359,11 @@ public class StreamDeckServerManager : MonoBehaviour Debug.LogError($"[StreamDeckServerManager] 카메라 전환 실패: {ex.Message}"); } } - + private void HandleGetCameraList(StreamDeckService service) { if (cameraManager == null) return; - + var response = new { type = "camera_list_response", @@ -379,40 +375,40 @@ public class StreamDeckServerManager : MonoBehaviour current_camera = cameraManager.GetCurrentCameraState() } }; - + string json = JsonConvert.SerializeObject(response); service.SendMessage(json); } - + private void HandleToggleItem(Dictionary message) { Debug.Log($"[StreamDeckServerManager] 아이템 토글 요청 수신"); - + if (itemController == null) { Debug.LogError("[StreamDeckServerManager] itemController가 null입니다!"); return; } - + try { if (message.ContainsKey("data")) { var dataObject = message["data"]; - + if (dataObject is Newtonsoft.Json.Linq.JObject jObject) { if (jObject.ContainsKey("item_index")) { var itemIndexToken = jObject["item_index"]; - + if (int.TryParse(itemIndexToken?.ToString(), out int itemIndex)) { if (itemIndex >= 0 && itemIndex < (itemController.itemGroups?.Count ?? 0)) { Debug.Log($"[StreamDeckServerManager] 아이템 그룹 {itemIndex}번 토글"); itemController.ToggleGroup(itemIndex); - + // 아이템 변경 알림 전송 NotifyItemChanged(); } @@ -436,14 +432,14 @@ public class StreamDeckServerManager : MonoBehaviour if (data.ContainsKey("item_index")) { var itemIndexObj = data["item_index"]; - + if (int.TryParse(itemIndexObj?.ToString(), out int itemIndex)) { if (itemIndex >= 0 && itemIndex < (itemController.itemGroups?.Count ?? 0)) { Debug.Log($"[StreamDeckServerManager] 아이템 그룹 {itemIndex}번 토글"); itemController.ToggleGroup(itemIndex); - + // 아이템 변경 알림 전송 NotifyItemChanged(); } @@ -477,36 +473,36 @@ public class StreamDeckServerManager : MonoBehaviour Debug.LogError($"[StreamDeckServerManager] 아이템 토글 실패: {ex.Message}"); } } - + private void HandleSetItem(Dictionary message) { Debug.Log($"[StreamDeckServerManager] 아이템 설정 요청 수신"); - + if (itemController == null) { Debug.LogError("[StreamDeckServerManager] itemController가 null입니다!"); return; } - + try { if (message.ContainsKey("data")) { var dataObject = message["data"]; - + if (dataObject is Newtonsoft.Json.Linq.JObject jObject) { if (jObject.ContainsKey("item_index")) { var itemIndexToken = jObject["item_index"]; - + if (int.TryParse(itemIndexToken?.ToString(), out int itemIndex)) { if (itemIndex >= 0 && itemIndex < (itemController.itemGroups?.Count ?? 0)) { Debug.Log($"[StreamDeckServerManager] 아이템 그룹 {itemIndex}번으로 설정"); itemController.Set(itemIndex); - + // 아이템 변경 알림 전송 NotifyItemChanged(); } @@ -530,14 +526,14 @@ public class StreamDeckServerManager : MonoBehaviour if (data.ContainsKey("item_index")) { var itemIndexObj = data["item_index"]; - + if (int.TryParse(itemIndexObj?.ToString(), out int itemIndex)) { if (itemIndex >= 0 && itemIndex < (itemController.itemGroups?.Count ?? 0)) { Debug.Log($"[StreamDeckServerManager] 아이템 {itemIndex}번으로 설정"); itemController.Set(itemIndex); - + // 아이템 변경 알림 전송 NotifyItemChanged(); } @@ -571,11 +567,11 @@ public class StreamDeckServerManager : MonoBehaviour Debug.LogError($"[StreamDeckServerManager] 아이템 설정 실패: {ex.Message}"); } } - + private void HandleGetItemList(StreamDeckService service) { if (itemController == null) return; - + var response = new { type = "item_list_response", @@ -587,7 +583,7 @@ public class StreamDeckServerManager : MonoBehaviour current_item = itemController.GetCurrentItemState() } }; - + string json = JsonConvert.SerializeObject(response); service.SendMessage(json); } @@ -596,7 +592,7 @@ public class StreamDeckServerManager : MonoBehaviour public class StreamDeckService : WebSocketBehavior { private StreamDeckServerManager serverManager; - + protected override void OnOpen() { serverManager = StreamDeckServerManager.Instance; @@ -606,18 +602,18 @@ public class StreamDeckService : WebSocketBehavior } Debug.Log("[StreamDeckService] WebSocket 연결 열림"); } - + protected override void OnMessage(WebSocketSharp.MessageEventArgs e) { Debug.Log($"[StreamDeckService] 원본 메시지 수신: {e.Data}"); - + // 메인 스레드에서 처리하도록 매니저에게 전달 if (serverManager != null) { serverManager.ProcessMessageOnMainThread(e.Data, this); } } - + // WebSocketSharp의 Send 메서드 래퍼 public void SendMessage(string message) { @@ -630,7 +626,7 @@ public class StreamDeckService : WebSocketBehavior Debug.LogError($"[StreamDeckService] 메시지 전송 실패: {ex.Message}"); } } - + protected override void OnClose(WebSocketSharp.CloseEventArgs e) { if (serverManager != null) @@ -639,9 +635,9 @@ public class StreamDeckService : WebSocketBehavior } Debug.Log($"[StreamDeckService] WebSocket 연결 닫힘: {e.Reason}"); } - + protected override void OnError(WebSocketSharp.ErrorEventArgs e) { Debug.LogError($"[StreamDeckService] WebSocket 오류: {e.Message}"); } -} \ No newline at end of file +} \ No newline at end of file diff --git a/StreamDock-Plugin-SDK/SDNodeJsSDKV2/com.mirabox.streamdock.demo.sdPlugin/plugin/index.js b/StreamDock-Plugin-SDK/SDNodeJsSDKV2/com.mirabox.streamdock.demo.sdPlugin/plugin/index.js index c4058203..f4466337 100644 --- a/StreamDock-Plugin-SDK/SDNodeJsSDKV2/com.mirabox.streamdock.demo.sdPlugin/plugin/index.js +++ b/StreamDock-Plugin-SDK/SDNodeJsSDKV2/com.mirabox.streamdock.demo.sdPlugin/plugin/index.js @@ -1,11 +1,90 @@ const { Plugins, Actions, log, EventEmitter } = require('./utils/plugin'); const { execSync } = require('child_process'); +const WebSocket = require('ws'); +const fs = require('fs'); +const path = require('path'); const plugin = new Plugins('demo'); +// 로그 파일 경로 설정 +const logFilePath = path.join(__dirname, 'streamdeck_plugin.log'); + +// 로그 함수 - 기존 log 시스템과 함께 사용 +function writeLog(message) { + const timestamp = new Date().toISOString(); + const logMessage = `[${timestamp}] ${message}`; + + // 기존 log 시스템 사용 + log.info(logMessage); + + // 파일에도 기록 + try { + fs.appendFileSync(logFilePath, logMessage + '\n'); + } catch (err) { + log.error('로그 파일 쓰기 실패:', err.message); + } +} + +const counters = {}; +let unityWebSocket = null; +let reconnectTimer = null; + +// 유니티 WebSocket 서버 연결 함수 +function connectToUnity() { + if (unityWebSocket) { + unityWebSocket.close(); + } + + unityWebSocket = new WebSocket('ws://localhost:10701/'); + + unityWebSocket.on('open', function() { + writeLog('유니티 WebSocket 서버에 연결됨'); + }); + + unityWebSocket.on('message', function(data) { + writeLog('유니티로부터 메시지 수신: ' + data.toString()); + }); + + unityWebSocket.on('error', function(err) { + writeLog('유니티 WebSocket 연결 오류: ' + err.message); + scheduleReconnect(); + }); + + unityWebSocket.on('close', function() { + writeLog('유니티 WebSocket 연결 종료'); + scheduleReconnect(); + }); +} + +// 재연결 스케줄링 +function scheduleReconnect() { + if (reconnectTimer) { + clearTimeout(reconnectTimer); + } + reconnectTimer = setTimeout(() => { + writeLog('유니티 WebSocket 서버 재연결 시도...'); + connectToUnity(); + }, 3000); +} + +// 유니티로 메시지 전송 +function sendToUnity(message) { + if (unityWebSocket && unityWebSocket.readyState === WebSocket.OPEN) { + unityWebSocket.send(message); + writeLog('유니티 WebSocket 서버로 메시지 전송: ' + message); + return true; + } else { + writeLog('유니티 WebSocket 서버 연결이 없거나 열려있지 않음'); + return false; + } +} plugin.didReceiveGlobalSettings = ({ payload: { settings } }) => { log.info('didReceiveGlobalSettings', settings); + writeLog('플러그인 설정 수신됨'); + + // 플러그인 시작 시 유니티 WebSocket 서버 연결 + connectToUnity(); }; const createSvg = (text) => ` @@ -20,22 +99,30 @@ plugin.demo = new Actions({ default: { }, async _willAppear({ context, payload }) { - // log.info("demo: ", context); - let n = 0; - timers[context] = setInterval(async () => { - const svg = createSvg(++n); - plugin.setImage(context, `data:image/svg+xml;charset=utf8,${svg}`); - }, 1000); + // 버튼이 처음 나타날 때 카운터 초기화 및 이미지 표시 + counters[context] = 0; + const svg = createSvg(counters[context]); + plugin.setImage(context, `data:image/svg+xml;charset=utf8,${svg}`); + writeLog(`버튼 나타남, context: ${context}`); }, _willDisappear({ context }) { - // log.info('willDisAppear', context) - timers[context] && clearInterval(timers[context]); + delete counters[context]; + writeLog(`버튼 사라짐, context: ${context}`); }, _propertyInspectorDidAppear({ context }) { }, sendToPlugin({ payload, context }) { }, keyUp({ context, payload }) { + // 카운터 증가 + if (counters[context] === undefined) counters[context] = 0; + counters[context]++; + const svg = createSvg(counters[context]); + plugin.setImage(context, `data:image/svg+xml;charset=utf8,${svg}`); + writeLog(`버튼 클릭됨, context: ${context}, count: ${counters[context]}`); + + // 유니티 WebSocket 서버로 메시지 전송 + sendToUnity('Hello Unity from StreamDeck!'); }, dialDown({ context, payload }) {}, dialRotate({ context, payload }) {} diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/images/camera_icon_inactive.png b/Streamdeck/com.mirabox.streamingle.sdPlugin/images/camera_icon_inactive.png new file mode 100644 index 00000000..a5d1fd20 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/images/camera_icon_inactive.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:01bb81cf3a75d7497de7a76beedf55af5f0e783900a0bef1634ad9d057cd15c1 +size 23961 diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/manifest.json b/Streamdeck/com.mirabox.streamingle.sdPlugin/manifest.json index 02c1f35a..4cef03ab 100644 --- a/Streamdeck/com.mirabox.streamingle.sdPlugin/manifest.json +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/manifest.json @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:26c553668887187f9f7c5272a323c8ea371635cb62e5bf72b5fc4767a14501f7 -size 2191 +oid sha256:0f84721f6fa44719b7d51065be665cd5427e60230a6f1bf7114b5cf2179296ca +size 2331 diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/plugin/index.js b/Streamdeck/com.mirabox.streamingle.sdPlugin/plugin/index.js index 5b201e28..f60cbc41 100644 --- a/Streamdeck/com.mirabox.streamingle.sdPlugin/plugin/index.js +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/plugin/index.js @@ -9,6 +9,18 @@ let isUnityConnected = false; let cameraList = []; // 카메라 목록 저장 let itemList = []; // 아이템 목록 저장 +const fs = require('fs'); + +function getBase64Image(filePath) { + try { + const data = fs.readFileSync(filePath); + return 'data:image/png;base64,' + data.toString('base64'); + } catch (e) { + console.error('이미지 파일 읽기 실패:', filePath, e); + return ''; + } +} + // StreamDock 연결 함수 (브라우저 기반) function connectElgatoStreamDeckSocket(inPort, inUUID, inEvent, inInfo, inActionInfo) { console.log('🔌 StreamDock 연결 시작 (브라우저)'); @@ -576,6 +588,12 @@ function handleUnityMessage(data) { 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 { @@ -586,7 +604,41 @@ function handleUnityMessage(data) { break; case 'camera_changed': - console.log('🎯 카메라 변경 알림'); + 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': @@ -705,6 +757,11 @@ function updateButtonTitle(context) { 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; @@ -750,8 +807,8 @@ function updateButtonTitle(context) { websocket.send(JSON.stringify(message)); console.log('🏷️ 버튼 제목 업데이트:', title, '(액션 타입:', actionType, ', 활성:', isActive, ')'); - // 아이템이 비활성화되어 있으면 아이콘을 어둡게 표시 - if (actionType === 'item' && !isActive) { + // 아이템이나 카메라가 비활성화되어 있으면 아이콘을 어둡게 표시 + if ((actionType === 'item' || actionType === 'camera') && !isActive) { setButtonState(context, false); // 비활성 상태로 설정 } else { setButtonState(context, true); // 활성 상태로 설정 @@ -768,8 +825,8 @@ function setButtonState(context, isActive) { const settings = getCurrentSettings(context); const actionType = settings.actionType || 'camera'; - // 아이템 컨트롤러만 상태 변경 적용 - if (actionType === 'item') { + // 아이템 컨트롤러와 카메라 컨트롤러 모두 상태 변경 적용 + if (actionType === 'item' || actionType === 'camera') { // 방법 1: setState 이벤트 사용 const stateMessage = { event: 'setState', @@ -783,19 +840,25 @@ function setButtonState(context, isActive) { websocket.send(JSON.stringify(stateMessage)); console.log('🎨 버튼 상태 업데이트 (setState):', context, '(활성:', isActive, ', 상태:', isActive ? 0 : 1, ')'); - // 방법 2: setImage 이벤트로 아이콘 직접 변경 - const imageName = isActive ? 'item_icon.png' : 'item_icon_inactive.png'; + // 방법 2: setImage 이벤트로 아이콘 직접 변경 (base64) + let imagePath; + if (actionType === 'item') { + imagePath = isActive ? 'images/item_icon.png' : 'images/item_icon_inactive.png'; + } else if (actionType === 'camera') { + imagePath = isActive ? 'images/camera_icon.png' : 'images/camera_icon_inactive.png'; + } + const imageBase64 = getBase64Image(imagePath); const imageMessage = { event: 'setImage', context: context, payload: { - image: imageName, + image: imageBase64, target: 0 // hardware and software } }; websocket.send(JSON.stringify(imageMessage)); - console.log('🖼️ 버튼 아이콘 업데이트 (setImage):', context, '(활성:', isActive, ', 이미지:', imageName, ')'); + console.log('🖼️ 버튼 아이콘 업데이트 (setImage):', context, '(활성:', isActive, ', 이미지:', imagePath, ')'); // 추가 디버깅을 위한 로그 setTimeout(() => {