v3.0.1: 에뮬레이터 업그레이드 — 정지/재시작, UTF-8 시리얼, CodeMirror, 속도슬라이더

코드 에디터 (code.html / code.js)
- xterm → @xterm/xterm 5.5.0 마이그레이션 (UMD 전역명 AddonFit/AddonWebLinks)
- Monaco Editor 0.52.0 업그레이드
- AI 패널 CSS 변수 (--ai-hdr-bg 등) 다크/라이트 테마 분리
- Arduino/GCC/Python/JS 오류 패턴(_ERROR_PATTERNS) 확장

Arduino 에뮬레이터 (arduino-emulator.html)
- CodeMirror 5 스케치 에디터 내장 (C++ 구문 강조, BroadcastChannel 동기화)
- 속도 select → range 슬라이더 (1x~최대, 로그 스케일)
- 시리얼 UTF-8: 수동 바이트버퍼 → 스트리밍 TextDecoder({ stream:true })로 교체 (°C 깨짐 수정)
- 정지/시작 토글 버튼 추가 (AVR: togglePower, ESP32/ESP8266: pause↔restart)
- 회로→코드 버튼 (🤖) 추가
- 보드 타이틀 FQBN 기반 동적 설정

서버 (server-v2.ts)
- QEMU/ESP8266 restart 엔드포인트: old PTY exit 핸들러가 새 세션을 삭제하던 레이스컨디션 수정 (s.pty = null 가드)
- ESP8266 /pause 엔드포인트 추가
- 정지 시 DELETE → pause로 변경 (세션·firmware 유지, 재시작 가능)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kim
2026-06-14 20:12:05 +09:00
co-authored by Claude Sonnet 4.6
parent 8b649e96d5
commit 18b9d5ba74
4 changed files with 8964 additions and 679 deletions
+323 -66
View File
@@ -133,7 +133,7 @@ function initCodeView() {
setCodeAiTech(codeAiTech);
if (monacoReady) { setTimeout(() => { if (monacoEditor) monacoEditor.layout(); }, 50); return; }
if (typeof require === 'undefined') return;
require.config({ paths: { vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs' } });
require.config({ paths: { vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.52.0/min/vs' } });
require(['vs/editor/editor.main'], function() {
monacoReady = true;
const isDark = document.documentElement.getAttribute('data-theme') !== 'light';
@@ -154,6 +154,17 @@ function initCodeView() {
tabSize: 2,
});
monacoEditor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, codeSaveFile);
// Broadcast .ino edits to emulator in real-time
let _syncDebounce = null;
monacoEditor.onDidChangeModelContent(() => {
if (_esp32SyncBusy) return;
clearTimeout(_syncDebounce);
_syncDebounce = setTimeout(() => {
const file = codeFiles.find(f => f.id === activeCodeFileId);
if (!file || !file.name.endsWith('.ino')) return;
_esp32CodeChannel.postMessage({ type: 'code', code: monacoEditor.getValue() });
}, 300);
});
codeFlushPendingQueue();
if (!codeFiles.length && !codePendingQueue.length) {
// Don't create a placeholder file — the model will create files as needed
@@ -490,12 +501,26 @@ function _termBufferAppend(shellId, raw) {
}
const _ERROR_PATTERNS = [
// Python
/Traceback \(most recent call last\)/,
/(?:Syntax|Name|Type|Attribute|Import|Indentation|Value|Key|Index|Zero|Overflow|Runtime|OS|IO|File)Error:/,
/^\s*\^\s*$/, // Python caret indicator
/^\s*\^\s*$/,
// JavaScript / Node
/Uncaught (?:TypeError|ReferenceError|SyntaxError|Error):/,
/Error: .+\n?\s+at /,
/\[Error\]/i,
// GCC / Arduino 컴파일 에러
/\berror:\s/,
/undefined reference to\b/,
/multiple definition of\b/,
/expected\s.+before\s/,
/invalid conversion from\b/,
/cannot convert\b/,
/sketch too big/i,
/\w+\.ino:\d+:\d+:.*error/,
/\w+\.cpp:\d+:\d+:.*error/,
/recipe for target.*failed/i,
/make\[1\].*Error/,
];
let _lastErrorShellId = null;
let _lastErrorText = '';
@@ -543,7 +568,7 @@ function _sendTerminalErrorToAI(errorText) {
const trimmed = filtered.slice(blockStart).slice(-30).join('\n');
if (!trimmed) return;
input.value = '다음 에러를 고쳐줘. coder_patch_file 도구로 파일을 직접 수정해:\n```\n' + trimmed + '\n```';
input.value = '다음 에러를 고쳐줘. Read로 파일을 읽은 뒤 Edit 또는 Write 도구로 파일을 직접 수정해:\n```\n' + trimmed + '\n```';
input.style.height = '';
input.dispatchEvent(new Event('input'));
codeAiSend();
@@ -737,8 +762,8 @@ function createXtermShell(container, shellId) {
fontFamily: "'IBM Plex Mono', 'D2Coding', 'NanumGothicCoding', 'Malgun Gothic', 'AppleGothic', monospace",
fontSize: 13, lineHeight: 1.4, cursorBlink: true, scrollback: 5000,
});
const fitAddon = new FitAddon.FitAddon();
const webLinksAddon = new WebLinksAddon.WebLinksAddon();
const fitAddon = new AddonFit.FitAddon();
const webLinksAddon = new AddonWebLinks.WebLinksAddon();
term.loadAddon(fitAddon); term.loadAddon(webLinksAddon);
term.open(sub); fitAddon.fit();
// Intercept Ctrl+` inside xterm to toggle the terminal panel.
@@ -887,32 +912,107 @@ function toggleCodePreview() {
function openArduinoEmulator() {
const file = codeFiles.find(f => f.id === activeCodeFileId);
const btn = document.getElementById('code-emu-btn');
if (!file || !file.name.endsWith('.ino')) { window.open('/arduino-emulator.html', 'arduino-emulator'); return; }
if (file && file.name.endsWith('.ino')) {
const content = file.model ? file.model.getValue() : (file.content || '');
const fqbn = _csbSelBoard.fqbn;
const fqbnArch = fqbn.split(':')[1] || '';
if (fqbnArch === 'esp32') {
// ESP32 계열 → QEMU (DIO 모드로 서버가 직접 컴파일)
// 팝업 차단 방지: 클릭 직후 즉시 창 열고, 완료 후 URL 교체
const emuWin = window.open('/arduino-emulator.html?loading=1', '_blank');
if (!emuWin) { codeAiAppend('system', '⚠️ 팝업 차단됨 — 브라우저에서 팝업을 허용해주세요'); return; }
if (btn) { btn.textContent = '⏳ 컴파일 중…'; btn.disabled = true; }
codeAiAppend('system', `🖥️ ESP32 에뮬레이터 컴파일 중... [${_csbSelBoard.name}]`);
// Store sketch code so emulator can show it (localStorage: shared across windows)
try { localStorage.setItem('esp32_sketch', content); } catch {}
api('/api/arduino/qemu/start', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ code: content, fqbn }) })
.then(d => {
if (btn) { btn.textContent = '🔌 에뮬레이터'; btn.disabled = false; }
if (d.error) {
emuWin.close();
const isUnsupported = d.error.includes('미지원') || d.error.includes('RISC-V');
if (isUnsupported) codeAiAppend('system', '⚠️ ' + d.error);
else sendCompileErrorToAI(d.error, file.name, content);
return;
}
const _proj = activeProjectName ? '&project=' + encodeURIComponent(activeProjectName) : '';
emuWin.location.href = `/arduino-emulator.html?session=${d.id}&file=${encodeURIComponent(file.name)}&fqbn=${encodeURIComponent(fqbn)}${_proj}`;
codeAiAppend('system', `✅ ESP32 에뮬레이터 실행 중.`);
}).catch(e => {
if (btn) { btn.textContent = '🔌 에뮬레이터'; btn.disabled = false; }
codeAiAppend('system', '⚠️ ' + e.message);
emuWin.close();
});
} else if (fqbnArch === 'esp8266') {
// ESP8266 → Native Linux x86 컴파일 + 실행
const emuWin = window.open('/arduino-emulator.html?loading=1', '_blank');
if (!emuWin) { codeAiAppend('system', '⚠️ 팝업 차단됨 — 브라우저에서 팝업을 허용해주세요'); return; }
if (btn) { btn.textContent = '⏳ 컴파일 중…'; btn.disabled = true; }
codeAiAppend('system', `🖥️ ESP8266 에뮬레이터 컴파일 중... [${_csbSelBoard.name}]`);
try { localStorage.setItem('esp8266_sketch', content); } catch {}
api('/api/arduino/esp8266/start', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ code: content, fqbn }) })
.then(d => {
if (btn) { btn.textContent = '🔌 에뮬레이터'; btn.disabled = false; }
if (d.error) {
emuWin.close();
sendCompileErrorToAI(d.error, file.name, content);
return;
}
const _proj = activeProjectName ? '&project=' + encodeURIComponent(activeProjectName) : '';
emuWin.location.href = `/arduino-emulator.html?session=${d.id}&file=${encodeURIComponent(file.name)}&fqbn=${encodeURIComponent(fqbn)}${_proj}`;
codeAiAppend('system', `✅ ESP8266 에뮬레이터 실행 중.`);
}).catch(e => {
if (btn) { btn.textContent = '🔌 에뮬레이터'; btn.disabled = false; }
codeAiAppend('system', '⚠️ ' + e.message);
emuWin.close();
});
} else {
// AVR 및 기타 → 기존 컴파일 → AVR 에뮬레이터 or 다운로드
if (btn) { btn.textContent = '⏳ 컴파일 중…'; btn.disabled = true; }
fetch('/api/arduino/compile', {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders({}) },
body: JSON.stringify({ code: file.content, board: 'uno' }),
body: JSON.stringify({ code: content, board: fqbn, fqbn }),
}).then(r => r.json()).then(data => {
if (btn) { btn.textContent = '🔌 에뮬레이터'; btn.disabled = false; }
if (data.autoInstalled?.length) codeAiAppend('system', `📦 라이브러리 자동 설치: ${data.autoInstalled.join(', ')}`);
if (data.error) { sendCompileErrorToAI(data.error, file.name, file.content); return; }
sessionStorage.setItem('arduino_sketch', file.content);
sessionStorage.setItem('arduino_hex', data.hex);
sessionStorage.setItem('arduino_filename', file.name);
sessionStorage.setItem('arduino_board', 'uno');
const w = window.open('/arduino-emulator.html', 'arduino-emulator');
if (!w) codeAiAppend('system', '⚠️ 팝업 차단됨 — 브라우저에서 팝업을 허용해주세요');
if (data.error) { sendCompileErrorToAI(data.error, file.name, content); return; }
if (data.hex) {
sessionStorage.setItem('arduino_hex', data.hex);
const w = window.open('/arduino-emulator.html', 'arduino-emulator');
if (!w) codeAiAppend('system', '⚠️ 팝업 차단됨 — 브라우저에서 팝업을 허용해주세요');
} else if (data.bin) {
const ext = data.boardType === 'uf2' ? '.uf2' : '.bin';
const blob = new Blob([Uint8Array.from(atob(data.bin), c => c.charCodeAt(0))], { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a'); a.href = url; a.download = file.name.replace(/\.ino$/, '') + ext; a.click();
URL.revokeObjectURL(url);
codeAiAppend('system', `✅ 컴파일 완료 (${(data.size/1024).toFixed(1)} KB) — ${ext.slice(1).toUpperCase()} 다운로드됨.\n보드에 올리려면 ⚡ 업로드 버튼을 사용하세요.`);
}
}).catch(e => {
if (btn) { btn.textContent = '🔌 에뮬레이터'; btn.disabled = false; }
codeAiAppend('system', '⚠️ 서버 오류: ' + e.message);
});
} else {
window.open('/arduino-emulator.html', 'arduino-emulator');
}
}
// ── Arduino 업로드 페이지 열기 ────────────────────────────────────────────────
function csbUploadToBoard() {
const file = codeFiles.find(f => f.id === activeCodeFileId);
if (!file || !file.name.endsWith('.ino')) {
codeAiAppend('system', '⚠️ .ino 파일을 선택해주세요'); return;
}
const content = file.model ? file.model.getValue() : (file.content || '');
sessionStorage.setItem('arduino_upload_code', content);
sessionStorage.setItem('arduino_upload_fqbn', _csbSelBoard.fqbn);
sessionStorage.setItem('arduino_upload_board', _csbSelBoard.name);
sessionStorage.setItem('arduino_upload_file', file.name);
const w = window.open('/arduino-upload.html', '_blank');
if (!w) codeAiAppend('system', '⚠️ 팝업 차단됨');
}
function codePreviewRefresh() {
const urlInput = document.getElementById('code-preview-url');
const iframe = document.getElementById('code-preview-iframe');
@@ -1315,33 +1415,48 @@ function codeRunWith(mode) {
}
if (mode === 'esp32') {
codeAiAppend('system', `⚡ ESP32 컴파일 중... (${file.name})`);
fetch('/api/arduino/compile', {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders({}) },
body: JSON.stringify({ code: file.content, board: 'esp32' }),
}).then(r => r.json()).then(data => {
if (data.autoInstalled?.length) codeAiAppend('system', `📦 라이브러리 자동 설치: ${data.autoInstalled.join(', ')}`);
if (data.error) {
sendCompileErrorToAI(data.error, file.name, file.content);
return;
}
sessionStorage.setItem('arduino_merged', data.merged);
sessionStorage.setItem('arduino_filename', file.name);
const w = window.open('/esp32-flasher.html', '_blank');
if (!w) codeAiAppend('system', '⚠️ 팝업 차단됨 — 브라우저에서 팝업을 허용해주세요');
else codeAiAppend('system', `✅ 컴파일 완료 (${(data.merged.length * 3/4 / 1024).toFixed(0)} KB). ESP32 Flasher 창에서 플래시하세요.`);
}).catch(e => codeAiAppend('system', '⚠️ 서버 오류: ' + e.message));
const emuWin = window.open('/arduino-emulator.html?loading=1', '_blank');
if (!emuWin) { codeAiAppend('system', '⚠️ 팝업 차단됨 — 브라우저에서 팝업을 허용해주세요'); return; }
codeAiAppend('system', `🖥️ ESP32 에뮬레이터 컴파일 중... (${file.name})`);
try { localStorage.setItem('esp32_sketch', file.content); } catch {}
api('/api/arduino/qemu/start', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ code: file.content, fqbn: 'esp32:esp32:esp32' }) })
.then(d => {
if (d.error) { sendCompileErrorToAI(d.error, file.name, file.content); emuWin.close(); return; }
const _proj2 = activeProjectName ? '&project=' + encodeURIComponent(activeProjectName) : '';
emuWin.location.href = `/arduino-emulator.html?session=${d.id}&file=${encodeURIComponent(file.name)}&fqbn=esp32%3Aesp32%3Aesp32${_proj2}`;
codeAiAppend('system', `✅ ESP32 에뮬레이터 실행 중.`);
}).catch(e => { codeAiAppend('system', '⚠️ ' + e.message); emuWin.close(); });
return;
}
if (mode === 'esp8266') {
const emuWin = window.open('/arduino-emulator.html?loading=1', '_blank');
if (!emuWin) { codeAiAppend('system', '⚠️ 팝업 차단됨 — 브라우저에서 팝업을 허용해주세요'); return; }
codeAiAppend('system', `🖥️ ESP8266 에뮬레이터 컴파일 중... (${file.name})`);
try { localStorage.setItem('esp8266_sketch', file.content); } catch {}
const _esp8266Fqbn = (_csbSelBoard.fqbn.startsWith('esp8266:')) ? _csbSelBoard.fqbn : 'esp8266:esp8266:nodemcuv2';
api('/api/arduino/esp8266/start', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ code: file.content, fqbn: _esp8266Fqbn }) })
.then(d => {
if (d.error) { sendCompileErrorToAI(d.error, file.name, file.content); emuWin.close(); return; }
const _proj2 = activeProjectName ? '&project=' + encodeURIComponent(activeProjectName) : '';
emuWin.location.href = `/arduino-emulator.html?session=${d.id}&file=${encodeURIComponent(file.name)}&fqbn=${encodeURIComponent(_esp8266Fqbn)}${_proj2}`;
codeAiAppend('system', `✅ ESP8266 에뮬레이터 실행 중.`);
}).catch(e => { codeAiAppend('system', '⚠️ ' + e.message); emuWin.close(); });
return;
}
if (mode === 'arduino') {
const board = 'uno';
codeAiAppend('system', `🔌 컴파일 중... (${file.name})`);
const _isEsp32Board = (_csbSelBoard.fqbn.split(':')[1] || '') === 'esp32';
const _isEsp8266Board = (_csbSelBoard.fqbn.split(':')[0] || '') === 'esp8266';
if (_isEsp32Board || _isEsp8266Board) {
// ESP32/ESP8266 계열 — 팝업 차단 방지를 위해 openArduinoEmulator()와 같은 경로 사용
openArduinoEmulator(); return;
}
codeAiAppend('system', `🔌 컴파일 중... (${file.name}) [${_csbSelBoard.name}]`);
fetch('/api/arduino/compile', {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders({}) },
body: JSON.stringify({ code: file.content, board }),
body: JSON.stringify({ code: file.content, board: _csbSelBoard.fqbn, fqbn: _csbSelBoard.fqbn }),
}).then(r => r.json()).then(data => {
if (data.autoInstalled?.length) codeAiAppend('system', `📦 라이브러리 자동 설치: ${data.autoInstalled.join(', ')}`);
if (data.error) {
@@ -1349,11 +1464,28 @@ function codeRunWith(mode) {
sendCompileErrorToAI(data.error + extra, file.name, file.content);
return;
}
sessionStorage.setItem('arduino_hex', data.hex);
sessionStorage.setItem('arduino_filename', file.name);
sessionStorage.setItem('arduino_board', board);
const w = window.open('/arduino-emulator.html', '_blank');
if (!w) codeAiAppend('system', '⚠️ 팝업 차단됨 — 브라우저에서 팝업을 허용해주세요');
sessionStorage.setItem('arduino_project', activeProjectName || '');
sessionStorage.setItem('arduino_board', _csbSelBoard.fqbn);
// boardType에 따라 결과 처리
if (data.merged) {
// (이미 위에서 처리됨, fallback)
codeAiAppend('system', `✅ 컴파일 완료.`);
} else if (data.hex) {
// AVR/ESP8266 → 에뮬레이터
sessionStorage.setItem('arduino_hex', data.hex);
const w = window.open('/arduino-emulator.html', '_blank');
if (!w) codeAiAppend('system', '⚠️ 팝업 차단됨 — 브라우저에서 팝업을 허용해주세요');
} else if (data.bin) {
const ext = data.boardType === 'uf2' ? '.uf2' : '.bin';
const blob = new Blob([Uint8Array.from(atob(data.bin), c => c.charCodeAt(0))], { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = file.name.replace(/\.ino$/, '') + ext; a.click();
URL.revokeObjectURL(url);
const kb = (data.size / 1024).toFixed(1);
codeAiAppend('system', `✅ 컴파일 완료 (${kb} KB) — ${ext.slice(1).toUpperCase()} 다운로드됨.\n보드에 올리려면 ⚡ 업로드 버튼을 사용하세요. [${_csbSelBoard.name}]`);
}
}).catch(e => codeAiAppend('system', '⚠️ 서버 오류: ' + e.message));
return;
}
@@ -1385,8 +1517,7 @@ function codeRunWith(mode) {
function sendCompileErrorToAI(error, filename, code) {
const inp = document.getElementById('code-ai-input');
if (!inp) return;
const codeBlock = code ? `\n\n코드:\n\`\`\`cpp\n${code}\n\`\`\`` : '';
inp.value = `아두이노 컴파일 오류가 발생했습니다. 원인을 설명하고 수정해줘.\n\n파일: ${filename}\n오류:\n\`\`\`\n${error}\n\`\`\`${codeBlock}`;
inp.value = `아두이노 컴파일 오류가 발생했습니다. Read로 ${filename}을 읽은 뒤 Edit 또는 Write 도구로 파일을 직접 수정해줘.\n\n오류:\n\`\`\`\n${error}\n\`\`\``;
codeAiSend();
}
@@ -2252,6 +2383,7 @@ async function _codeAiSendImpl() {
// ── Live server log bubble ────────────────────────────────────────────────
let _logBubble = null;
let _logPollTimer = null;
let _logBubbleStartCount = 0; // totalCount watermark — only show logs from after bubble appeared
function _showLogBubble() {
if (_logBubble) return;
@@ -2259,11 +2391,14 @@ function _showLogBubble() {
if (!msgs) return;
_logBubble = document.createElement('div');
_logBubble.className = 'code-ai-log-bubble';
_logBubble.innerHTML = '<span class="log-live-dot"></span><span class="log-lines">서버 연결 중...</span>';
_logBubble.innerHTML = '<span class="log-live-dot"></span><span class="log-lines"></span>';
msgs.appendChild(_logBubble);
codeAiScrollBottom();
_pollLogBubble();
_logPollTimer = setInterval(_pollLogBubble, 800);
// Record current totalCount so we only show logs that arrive during AI processing
fetch('/api/logs', { headers: authHeaders({}) })
.then(r => r.json()).then(d => { _logBubbleStartCount = d.totalCount || 0; })
.catch(() => {})
.finally(() => { _logPollTimer = setInterval(_pollLogBubble, 800); });
}
async function _pollLogBubble() {
@@ -2272,9 +2407,12 @@ async function _pollLogBubble() {
const r = await fetch('/api/logs', { headers: authHeaders({}) });
if (!r.ok) return;
const data = await r.json();
const lines = (data.entries || []).filter(e => !/^\[v\d\]|^\[task\]|^\[orch\]/.test(e.msg)).slice(-4).map(e => e.msg.slice(0, 90));
const allEntries = data.entries || [];
const newCount = Math.max(0, (data.totalCount || 0) - _logBubbleStartCount);
const newEntries = newCount > 0 ? allEntries.slice(-Math.min(newCount, allEntries.length)) : [];
const lines = newEntries.filter(e => !/^\[v\d\]|^\[task\]|^\[orch\]/.test(e.msg)).slice(-4).map(e => e.msg.slice(0, 90));
const linesEl = _logBubble.querySelector('.log-lines');
if (linesEl) linesEl.textContent = lines.join('\n') || '...';
if (linesEl) linesEl.textContent = lines.join('\n');
codeAiScrollBottom();
} catch {}
}
@@ -2594,9 +2732,14 @@ function _buildCodeSystemPrompt() {
- 프로젝트 루트 기준 상대 경로만 사용하세요. 예: \`main.ino\`, \`src/utils.cpp\`, \`lib/helper.h\`
- 새 프로젝트를 만들면 해당 폴더가 루트입니다. 별도의 상위 폴더를 만들지 마세요.
## 회로도 파일 규칙 (중요)
- \`circuit.json\` (회로도)은 사용자가 **명시적으로 회로도 편집을 요청**할 때만 읽거나 수정하세요.
- "TFT 연결해줘", "회로도에 부품 추가해줘" 같이 회로도 편집을 직접 언급한 경우에만 해당.
- 코드 작성·분석·업로드 등 일반 작업 중에는 circuit.json을 열지 마세요.
## 응답 원칙
- 행동 중심으로 응답하세요: 설명만 하지 말고 실제로 파일을 수정하세요.
- 변경 사항은 간결하게 설명하고, 코드 블록으로 전체 내용을 반복하지 마세요.
- **코드 변경은 반드시 Edit 또는 Write 도구로만** 하세요. 텍스트로 "수정했습니다", "수정 완료", 마크다운 코드 블록 출력으로 파일이 바뀌지 않습니다.
- 변경 사항은 도구 호출 후 한 줄로 간결하게 설명하세요.
- 여러 파일을 수정해야 한다면 순서대로 각각 처리하세요.`;
}
@@ -4150,6 +4293,116 @@ async function csbLibUninstall(name) {
}
// ── 보드 매니저 ───────────────────────────────────────────────────────────────
// ── 보드 선택기 ───────────────────────────────────────────────────────────────
const CSB_BOARD_DEFAULT = { fqbn: 'arduino:avr:uno', name: 'Arduino UNO' };
let _csbSelBoard = (() => {
try { return JSON.parse(localStorage.getItem('csb_selected_board') || 'null') || CSB_BOARD_DEFAULT; }
catch { return CSB_BOARD_DEFAULT; }
})();
let _csbSelAllBoards = [];
function _csbBoardSelSave(board) {
_csbSelBoard = board;
try { localStorage.setItem('csb_selected_board', JSON.stringify(board)); } catch {}
const el = document.getElementById('csb-board-sel-name');
if (el) el.textContent = board.name;
const badge = document.getElementById('csb-board-qemu-badge');
if (badge) {
if (board.qemu === 'esp8266-native') {
badge.textContent = '🖥 Native'; badge.style.display = 'inline';
badge.style.background = '#0c3a4a'; badge.style.color = '#67e8f9'; badge.style.borderColor = '#0e7490';
} else if (board.qemu) {
badge.textContent = '🖥 QEMU'; badge.style.display = 'inline';
badge.style.background = board.qemu === 'esp32s3' ? '#1a3a5a' : '#1a3a1a';
badge.style.color = board.qemu === 'esp32s3' ? '#60a5fa' : '#22c55e';
badge.style.borderColor = board.qemu === 'esp32s3' ? '#2a5a8a' : '#2a5a2a';
} else {
badge.style.display = 'none';
}
}
}
function csbBoardSelOpen() {
const dd = document.getElementById('csb-board-sel-dropdown');
if (!dd) return;
const open = dd.style.display !== 'none';
dd.style.display = open ? 'none' : 'flex';
if (!open) {
const q = document.getElementById('csb-board-sel-q');
if (q) { q.value = ''; q.focus(); }
csbBoardSelSearch();
}
}
function csbBoardSelClose() {
const dd = document.getElementById('csb-board-sel-dropdown');
if (dd) dd.style.display = 'none';
}
async function csbBoardSelSearch() {
const q = (document.getElementById('csb-board-sel-q')?.value || '').trim();
const list = document.getElementById('csb-board-sel-list');
if (!list) return;
list.innerHTML = '<div style="padding:10px;color:var(--muted);font-size:11px;text-align:center">불러오는 중...</div>';
try {
const data = await api('/api/arduino/boards/listall?q=' + encodeURIComponent(q));
const boards = data.boards || [];
if (!boards.length) { list.innerHTML = '<div style="padding:10px;color:var(--muted);font-size:11px;text-align:center">검색 결과 없음</div>'; return; }
list.innerHTML = boards.map(b => {
const sel = b.fqbn === _csbSelBoard.fqbn;
const qemuBadge = b.qemu === 'esp8266-native'
? `<span style="font-size:9px;padding:1px 5px;border-radius:3px;margin-left:4px;background:#0c3a4a;color:#67e8f9;border:1px solid #0e7490">🖥 Native</span>`
: b.qemu
? `<span style="font-size:9px;padding:1px 5px;border-radius:3px;margin-left:4px;
background:${b.qemu==='esp32s3'?'#1a3a5a':'#1a3a1a'};
color:${b.qemu==='esp32s3'?'#60a5fa':'#22c55e'};
border:1px solid ${b.qemu==='esp32s3'?'#2a5a8a':'#2a5a2a'}">🖥 QEMU</span>`
: '';
const varBadge = (b.variants > 1)
? `<span style="font-size:9px;padding:1px 5px;border-radius:3px;margin-left:4px;background:var(--panel-2);color:var(--muted);border:1px solid var(--line)">+${b.variants-1}</span>`
: '';
return `<div data-fqbn="${_csbEsc(b.fqbn)}" data-name="${_csbEsc(b.name)}"
data-qemu="${b.qemu||''}" data-chip="${b.chip||''}"
style="padding:7px 10px;cursor:pointer;font-size:11px;border-bottom:1px solid var(--line);display:flex;flex-direction:column;gap:2px;${sel?'background:var(--brand);color:#fff':''}">
<span style="display:flex;align-items:center;flex-wrap:wrap;gap:0">
<span style="font-weight:600;${sel?'color:#fff':'color:var(--text)'}">${_csbEsc(b.name)}</span>
${sel?'':qemuBadge}${sel?'':varBadge}
</span>
<span style="font-size:10px;${sel?'color:rgba(255,255,255,.75)':'color:var(--muted)'}">${_csbEsc(b.fqbn)}</span>
</div>`;
}).join('');
list.querySelectorAll('[data-fqbn]').forEach(el => {
el.addEventListener('click', () => {
_csbBoardSelSave({ fqbn: el.dataset.fqbn, name: el.dataset.name,
qemu: el.dataset.qemu || false, chip: el.dataset.chip || '' });
csbBoardSelClose();
});
el.addEventListener('mouseenter', () => { if (el.dataset.fqbn !== _csbSelBoard.fqbn) el.style.background = 'var(--bg)'; });
el.addEventListener('mouseleave', () => { if (el.dataset.fqbn !== _csbSelBoard.fqbn) el.style.background = ''; });
});
} catch(e) { list.innerHTML = `<div style="padding:10px;color:var(--muted);font-size:11px;text-align:center">오류: ${e.message}</div>`; }
}
function csbBoardSelKey(e) {
if (e.key === 'Escape') csbBoardSelClose();
}
// 드롭다운 외부 클릭 시 닫기
document.addEventListener('click', e => {
const display = document.getElementById('csb-board-sel-display');
const dd = document.getElementById('csb-board-sel-dropdown');
if (dd && display && !display.contains(e.target) && !dd.contains(e.target))
dd.style.display = 'none';
});
// 초기 표시 업데이트
(function() {
const el = document.getElementById('csb-board-sel-name');
if (el) el.textContent = _csbSelBoard.name;
// 저장된 보드에 qemu 정보가 있으면 배지 표시
if (_csbSelBoard.qemu) _csbBoardSelSave(_csbSelBoard);
})();
let _csbBoardCurrentTab = 'installed';
function csbBoardTab(tab) {
@@ -4162,7 +4415,7 @@ function csbBoardTab(tab) {
if (sr) sr.style.display = tab === 'search' ? 'flex' : 'none';
document.getElementById('csb-board-status').textContent = '';
if (tab === 'installed') csbBoardLoad();
else { document.getElementById('csb-board-list').innerHTML = '<div style="padding:16px;color:var(--muted);font-size:11px;text-align:center">키워드를 입력하세요.</div>'; }
else csbBoardSearch();
}
function _csbBoardStatus(msg) { const el = document.getElementById('csb-board-status'); if (el) el.textContent = msg; }
@@ -4170,10 +4423,9 @@ function _csbBoardStatus(msg) { const el = document.getElementById('csb-board-st
async function csbBoardLoad() {
document.getElementById('csb-board-list').innerHTML = '<div style="padding:16px;color:var(--muted);font-size:11px;text-align:center">불러오는 중...</div>';
try {
const r = await fetch('/api/arduino/cores', { headers: authHeaders() });
const data = await r.json();
const data = await api('/api/arduino/cores');
_csbBoardRenderInstalled(data.cores || []);
} catch { document.getElementById('csb-board-list').innerHTML = '<div style="padding:16px;color:var(--muted);font-size:11px;text-align:center">오류</div>'; }
} catch(e) { document.getElementById('csb-board-list').innerHTML = `<div style="padding:16px;color:var(--muted);font-size:11px;text-align:center">오류: ${e.message}</div>`; }
}
function _csbBoardRenderInstalled(cores) {
@@ -4201,10 +4453,9 @@ async function csbBoardSearch() {
document.getElementById('csb-board-list').innerHTML = '<div style="padding:16px;color:var(--muted);font-size:11px;text-align:center">검색 중...</div>';
_csbBoardStatus('');
try {
const r = await fetch('/api/arduino/cores/search?q=' + encodeURIComponent(q), { headers: authHeaders() });
const data = await r.json();
const data = await api('/api/arduino/cores/search?q=' + encodeURIComponent(q));
_csbBoardRenderSearch(data.cores || []);
} catch { document.getElementById('csb-board-list').innerHTML = '<div style="padding:16px;color:var(--muted);font-size:11px;text-align:center">오류</div>'; }
} catch(e) { document.getElementById('csb-board-list').innerHTML = `<div style="padding:16px;color:var(--muted);font-size:11px;text-align:center">오류: ${e.message}</div>`; }
}
function _csbBoardRenderSearch(cores) {
@@ -4232,11 +4483,7 @@ async function csbBoardInstall(id, btn) {
_csbBoardStatus(`설치 중: ${id} (수 분 소요)...`);
if (btn) { btn.disabled = true; btn.textContent = '...'; }
try {
const r = await fetch('/api/arduino/cores/install', {
method: 'POST', headers: { ...authHeaders(), 'Content-Type': 'application/json' },
body: JSON.stringify({ id }),
});
const data = await r.json();
const data = await api('/api/arduino/cores/install', { method: 'POST', body: JSON.stringify({ id }) });
if (data.error) { _csbBoardStatus('오류: ' + data.error.slice(0,150)); if(btn){btn.disabled=false;btn.textContent='설치';} return; }
_csbBoardStatus(`✅ ${id} 설치 완료`);
if (btn) btn.outerHTML = `<span style="font-size:10px;color:var(--brand);border:1px solid;border-radius:4px;padding:2px 6px">✅ 설치됨</span>`;
@@ -4247,11 +4494,7 @@ async function csbBoardUninstall(id) {
if (!confirm(`"${id}" 보드를 삭제할까요?`)) return;
_csbBoardStatus(`삭제 중: ${id}...`);
try {
const r = await fetch('/api/arduino/cores/uninstall', {
method: 'DELETE', headers: { ...authHeaders(), 'Content-Type': 'application/json' },
body: JSON.stringify({ id }),
});
const data = await r.json();
const data = await api('/api/arduino/cores/uninstall', { method: 'DELETE', body: JSON.stringify({ id }) });
if (data.error) { _csbBoardStatus('오류: ' + data.error.slice(0,100)); return; }
_csbBoardStatus(`✅ ${id} 삭제 완료`);
csbBoardLoad();
@@ -5172,6 +5415,20 @@ function setupCodeVResize() {
// ── Standalone bootstrap ────────────────────────────────────────────────────
// In the main app these are triggered by setMode('code') / setSidebarTab('code').
// BroadcastChannel: real-time code sync between code editor and emulator
const _esp32CodeChannel = new BroadcastChannel('esp32_code_sync');
let _esp32SyncBusy = false;
_esp32CodeChannel.onmessage = (e) => {
if (e.data?.type !== 'code') return;
const file = codeFiles.find(f => f.id === activeCodeFileId);
if (!file || !file.name.endsWith('.ino') || !monacoEditor) return;
_esp32SyncBusy = true;
const pos = monacoEditor.getPosition();
if (file.model) file.model.setValue(e.data.code);
if (pos) monacoEditor.setPosition(pos);
_esp32SyncBusy = false;
};
// Here we run them once the DOM is ready since this page IS the code view.
(function bootCodePage() {
async function boot() {