v3.0.2: server-v2 모듈 분리 — Arduino·PPTX 라우트 추출 (-2923줄)

- routes-arduino.ts: Arduino 컴파일/라이브러리/QEMU/ESP8266 에뮬 전체
- routes-pptx.ts: PPTX 생성/편집/PDF/이미지 API 전체
- mocks-esp32: WiFi.STA(_STAProxy), scanNetworks, DELETE() 수정
- mocks-esp8266: DELETE() 수정
- server-v2.ts: 16106 → 13183줄 (–18%)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kim
2026-06-16 21:00:40 +09:00
co-authored by Claude Sonnet 4.6
parent 18b9d5ba74
commit a994ac1b73
9 changed files with 6715 additions and 4851 deletions
+184 -17
View File
@@ -762,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 AddonFit.FitAddon();
const webLinksAddon = new AddonWebLinks.WebLinksAddon();
const fitAddon = new FitAddon.FitAddon();
const webLinksAddon = new WebLinksAddon.WebLinksAddon();
term.loadAddon(fitAddon); term.loadAddon(webLinksAddon);
term.open(sub); fitAddon.fit();
// Intercept Ctrl+` inside xterm to toggle the terminal panel.
@@ -1514,10 +1514,86 @@ function codeRunWith(mode) {
}, codeTermOpen ? 0 : 500);
}
function _boardSpecificHints(code = '') {
const fqbn = _csbSelBoard.fqbn || '';
const arch = fqbn.split(':')[1] || '';
const usesWifi = /WiFi|ESP8266WiFi|HTTPClient|WebServer/i.test(code);
if (arch === 'esp32') {
const lines = [
'ESP32 코딩 규칙 (반드시 준수):',
'- analogWrite() 없음 → ledcAttach(pin, freq, res) + ledcWrite(pin, duty) 사용',
'- loop()에서 오래 blocking하면 watchdog(TG1WDT) 리셋 발생 → delay() 또는 yield() 필수',
'- SPIFFS 대신 LittleFS 권장 (esp32 3.x 코어)',
];
if (usesWifi) lines.splice(1, 0,
'- WiFi는 SoC 내장 — #include <SPI.h> 절대 추가하지 말 것',
'- #include <WiFi.h> 하나로 충분 (esp32 코어 제공)',
'- WiFi.firmwareVersion() 없음 (WiFiNINA 전용)');
return '\n' + lines.join('\n');
}
if (arch === 'esp8266') {
const lines = [
'ESP8266 코딩 규칙 (반드시 준수):',
'- analogWrite() 있음 (PWM, 0-1023)',
'- loop()에서 yield() 또는 delay() 호출 필수 (watchdog 방지)',
'- SPI.h / ESP32Servo.h / ledcAttach 등 ESP32 전용 API 사용 금지',
];
if (usesWifi) lines.splice(1, 0,
'- WiFi: #include <ESP8266WiFi.h> — #include <WiFi.h> 절대 사용 금지',
'- WiFi 암호화 상수: ENC_TYPE_NONE / ENC_TYPE_CCMP / ENC_TYPE_TKIP 사용 (WIFI_AUTH_OPEN 등 ESP32 상수 금지)',
'- WiFi 스캔: WiFi.scanNetworks() / WiFi.getNetworkInfo() / WiFi.getScanInfoByIndex() / bss_info 사용');
return '\n' + lines.join('\n');
}
if (arch === 'avr') {
const lines = ['AVR(Arduino) 코딩 규칙:'];
if (usesWifi) lines.push('- WiFi 실드: #include <SPI.h> + #include <WiFiNINA.h> 또는 #include <WiFi.h>');
return '\n' + lines.join('\n');
}
return '';
}
async function csbCompileOnly() {
if (!_csbSelBoard) { codeAiAppend('system', '⚠ 보드를 먼저 선택하세요.'); return; }
const file = codeFiles.find(f => f.id === activeCodeFileId);
if (!file || !file.name.endsWith('.ino')) { codeAiAppend('system', '⚠ .ino 파일을 선택하세요.'); return; }
const code = file.model?.getValue() || file.content || '';
const btn = document.getElementById('code-compile-btn');
if (btn) { btn.textContent = '⏳ 컴파일 중…'; btn.disabled = true; }
codeAiAppend('system', `🔨 컴파일 중... [${_csbSelBoard.name}]`);
try {
const r = await fetch('/api/arduino/compile', {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders() },
body: JSON.stringify({ code, fqbn: _csbSelBoard.fqbn }),
});
const data = await r.json();
if (data.error) {
sendCompileErrorToAI(data.error, file.name, code);
} else {
const kb = data.size ? (data.size / 1024).toFixed(1) : null;
const extra = data.autoInstalled?.length ? `\n자동 설치된 라이브러리: ${data.autoInstalled.join(', ')}` : '';
codeAiAppend('system', `✅ 컴파일 성공${kb ? ` (${kb} KB)` : ''} — 오류 없음.${extra}`);
}
} catch (e) {
codeAiAppend('system', `컴파일 오류: ${e.message}`);
} finally {
if (btn) { btn.textContent = '🔨 컴파일'; btn.disabled = false; }
}
}
function sendCompileErrorToAI(error, filename, code) {
const inp = document.getElementById('code-ai-input');
if (!inp) return;
inp.value = `아두이노 컴파일 오류가 발생했습니다. Read로 ${filename}을 읽은 뒤 Edit 또는 Write 도구로 파일을 직접 수정해줘.\n\n오류:\n\`\`\`\n${error}\n\`\`\``;
const boardInfo = `대상 보드: ${_csbSelBoard.name} (FQBN: ${_csbSelBoard.fqbn})`;
const hints = _boardSpecificHints(code);
// Library conflict (redefinition) — not fixable by editing the .ino file
if (/redefinition of/.test(error)) {
const m = error.match(/\/sketch\/(\S+?\.h)/);
const conflictHeader = m ? m[1] : 'unknown.h';
inp.value = `아두이노 컴파일 오류: **라이브러리 충돌**입니다. ${boardInfo}${hints}\n\n원인: \`${conflictHeader}\`가 코어 내장 헤더와 중복 정의(redefinition) 충돌을 일으키고 있습니다. ${filename} 코드의 \`#include\` 방향을 조정하거나 외부 라이브러리를 교체해야 합니다.\n\n오류:\n\`\`\`\n${error}\n\`\`\`\n\n해결 방법을 설명해주세요. 파일 검색은 하지 않아도 됩니다.`;
} else {
inp.value = `아두이노 컴파일 오류가 발생했습니다. ${boardInfo}${hints}\n\nRead로 ${filename}을 읽은 뒤 Edit 또는 Write 도구로 파일을 직접 수정해줘.\n\n오류:\n\`\`\`\n${error}\n\`\`\``;
}
codeAiSend();
}
@@ -2711,6 +2787,9 @@ async function _codeAiOneRound(messages, onToken) {
function _buildCodeSystemPrompt() {
const localNote = localDirHandle ? `로컬 폴더 "${localDirHandle.name}"이 연결되어 있습니다.` : '로컬 폴더가 연결되지 않았습니다.';
const summaryBlock = _historySummary ? `\n[이전 대화 요약]\n${_historySummary}\n` : '';
const _activeCode = (() => { try { const f = codeFiles.find(f => f.id === activeCodeFileId); return f?.model?.getValue() || f?.content || ''; } catch { return ''; } })();
const boardHints = _boardSpecificHints(_activeCode);
const boardBlock = boardHints ? `\n## 대상 보드: ${_csbSelBoard.name} (${_csbSelBoard.fqbn})${boardHints}\n` : '';
const karpathyBlock = _karpathyMode ? `
## Karpathy 코딩 가이드라인 (필수)
1. Think Before Coding — 가정을 명시하고, 모호하면 질문하세요. 여러 해석 가능 시 제시 후 선택하세요.
@@ -2718,7 +2797,7 @@ function _buildCodeSystemPrompt() {
3. Surgical Changes — 사용자 요청과 직접 관련 없는 코드는 건드리지 마세요. 기존 스타일을 그대로 따르세요. 내 변경으로 인한 고아 코드만 제거하세요.
4. Goal-Driven Execution — 작업을 검증 가능한 목표로 변환하세요. "버그 수정" → "재현 테스트 작성 → 통과".
` : '';
return `당신은 전문 코드 어시스턴트입니다. ${localNote}${summaryBlock}${karpathyBlock}
return `당신은 전문 코드 어시스턴트입니다. ${localNote}${summaryBlock}${boardBlock}${karpathyBlock}
## 툴 사용 지침
- 파일을 수정하기 전에 반드시 Read로 현재 내용을 확인하세요.
@@ -2726,6 +2805,7 @@ function _buildCodeSystemPrompt() {
- **Write**: 수정이 3곳 이상이거나 전체 리팩토링·일괄 변경 시 사용. Read로 읽은 내용을 수정한 뒤 Write로 교체하세요. Edit 반복보다 훨씬 빠릅니다.
- Glob으로 파일 목록 검색, Grep으로 코드 내용 검색이 가능합니다.
- Bash는 companion.py 연결 시 터미널 명령을 실행합니다.
- **파일을 2번 검색해도 없으면 즉시 멈추세요.** 같은 패턴으로 반복 검색하지 말고, 파일이 없다는 사실을 사용자에게 설명한 뒤 다른 해결책을 제안하세요.
## 파일 경로 규칙 (중요)
- 파일 경로에 절대경로·Windows 경로를 절대 사용하지 마세요: Documents/, Desktop/, C:\\, /home/ 등 금지.
@@ -4145,6 +4225,8 @@ function setCodeSbTab(tab) {
// ── 라이브러리/보드 매니저 (code editor sidebar) ──────────────────────────────
let _csbLibCurrentTab = 'installed';
let _csbLibFilterMode = 'all'; // 'all' | 'compat' | 'incompat'
let _csbLibCache = [];
let _csbArdSection = 'lib';
function csbArdSection(sec) {
@@ -4167,11 +4249,26 @@ function csbLibTab(tab) {
});
const sr = document.getElementById('csb-lib-search-row');
if (sr) sr.style.display = tab === 'search' ? 'flex' : 'none';
const fr = document.getElementById('csb-lib-filter-row');
if (fr) fr.style.display = tab === 'installed' ? 'flex' : 'none';
document.getElementById('csb-lib-status').textContent = '';
if (tab === 'installed') csbLibLoad();
else _csbLibListEl().innerHTML = '<div style="padding:16px;color:var(--muted);font-size:11px;text-align:center">키워드를 입력하세요.</div>';
}
function csbLibFilter(mode) {
_csbLibFilterMode = mode;
['all','compat','incompat'].forEach(m => {
const btn = document.getElementById('csb-libf-' + m);
if (!btn) return;
const active = m === mode;
btn.style.background = active ? 'var(--brand)' : 'var(--panel-2)';
btn.style.color = active ? '#fff' : 'var(--muted)';
btn.style.borderColor = active ? 'var(--brand)' : 'var(--line)';
});
_csbLibRenderInstalled(_csbLibCache);
}
function _csbLibListEl() { return document.getElementById('csb-lib-list'); }
function _csbLibStatus(msg) { const el = document.getElementById('csb-lib-status'); if (el) el.textContent = msg; }
function _csbEsc(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;'); }
@@ -4179,36 +4276,95 @@ function _csbEsc(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;'
async function csbLibLoad() {
_csbLibListEl().innerHTML = '<div style="padding:16px;color:var(--muted);font-size:11px;text-align:center">불러오는 중...</div>';
try {
const r = await fetch('/api/arduino/libs', { headers: authHeaders() });
const fqbn = _csbSelBoard?.fqbn ? '&fqbn=' + encodeURIComponent(_csbSelBoard.fqbn) : '';
const r = await fetch('/api/arduino/libs?all=1' + fqbn, { headers: authHeaders() });
const data = await r.json();
_csbLibRenderInstalled(data.libs || []);
_csbLibCache = data.libs || [];
_csbLibRenderInstalled(_csbLibCache);
} catch { _csbLibListEl().innerHTML = '<div style="padding:16px;color:var(--muted);font-size:11px;text-align:center">오류</div>'; }
}
function _csbArchBadge(architectures) {
const arch = _csbSelBoard.fqbn.split(':')[1] || '';
const archs = architectures || [];
const isAll = archs.includes('*') || archs.length === 0;
const compat = isAll || archs.some(a => a === arch || a.startsWith(arch) || arch.startsWith(a));
if (isAll) return `<span style="font-size:9px;padding:1px 5px;border-radius:3px;background:#1a2a1a;color:#6ee7b7;border:1px solid #2d4a2d">모든 보드</span>`;
const label = archs.slice(0,3).join(', ') + (archs.length>3?'…':'');
const color = compat ? '#6ee7b7' : '#f87171';
const bg = compat ? '#1a2a1a' : '#2a1a1a';
const bd = compat ? '#2d4a2d' : '#4a2d2d';
const icon = compat ? '✓' : '✗';
return `<span style="font-size:9px;padding:1px 5px;border-radius:3px;background:${bg};color:${color};border:1px solid ${bd}" title="호환 아키텍처: ${archs.join(', ')}">${icon} ${_csbEsc(label)}</span>`;
}
function _csbIsCompat(l) {
const arch = _csbSelBoard.fqbn.split(':')[1] || '';
const archs = l.architectures || [];
return archs.includes('*') || archs.length === 0 || archs.some(a => a === arch || a.startsWith(arch) || arch.startsWith(a));
}
function _csbLibRenderInstalled(libs) {
const el = _csbLibListEl();
if (!libs.length) { el.innerHTML = '<div style="padding:16px;color:var(--muted);font-size:11px;text-align:center">설치된 라이브러리 없음</div>'; return; }
el.innerHTML = libs.map(l => {
// Apply filter
let filtered = libs;
if (_csbLibFilterMode === 'compat') filtered = libs.filter(l => _csbIsCompat(l));
if (_csbLibFilterMode === 'incompat') filtered = libs.filter(l => !_csbIsCompat(l));
// Sort: platform/core first, then compatible, then incompatible
if (_csbLibFilterMode === 'all') {
filtered = [...libs].sort((a, b) => {
const pa = a.location === 'platform', pb = b.location === 'platform';
if (pa && !pb) return -1;
if (!pa && pb) return 1;
const ca = _csbIsCompat(a), cb = _csbIsCompat(b);
if (ca && !cb) return -1;
if (!ca && cb) return 1;
return a.name.localeCompare(b.name);
});
}
if (!filtered.length) {
const msg = _csbLibFilterMode === 'compat' ? `${_csbSelBoard.name}과 호환되는 라이브러리 없음` : '비호환 라이브러리 없음';
el.innerHTML = `<div style="padding:16px;color:var(--muted);font-size:11px;text-align:center">${msg}</div>`;
return;
}
const fqbn = _csbSelBoard?.fqbn || '';
el.innerHTML = filtered.map(l => {
const sid = 'csbex_' + l.name.replace(/\W/g,'_');
const archs = l.architectures || [];
const compat = _csbIsCompat(l);
const isPlatform = l.location === 'platform';
const incompatWarn = !compat ? `<div style="font-size:10px;color:#f87171;margin-top:3px">⚠ 현재 보드(${_csbEsc(_csbSelBoard.name)})와 호환되지 않을 수 있음</div>` : '';
const platformBadge = isPlatform ? `<span style="font-size:9px;padding:1px 5px;border-radius:3px;background:#1a2a3a;color:#60a5fa;border:1px solid #2d4a6a">코어</span>` : '';
const exRows = (l.examples||[]).map(ex =>
`<div style="display:flex;align-items:center;padding:3px 0;gap:6px">
<span style="font-size:10px;color:var(--muted);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">📄 ${_csbEsc(ex)}</span>
<button onclick="csbLibOpenExample('${_csbEsc(l.name)}','${_csbEsc(ex)}')"
style="font-size:10px;padding:2px 7px;background:none;border:1px solid var(--line);color:var(--brand);border-radius:4px;cursor:pointer;font-family:inherit;white-space:nowrap">열기</button>
<button onclick="csbLibOpenExample('${_csbEsc(l.name)}','${_csbEsc(ex)}',${compat},'${_csbEsc(fqbn)}')"
style="font-size:10px;padding:2px 7px;background:none;border:1px solid var(--line);color:${compat?'var(--brand)':'#f87171'};border-radius:4px;cursor:pointer;font-family:inherit;white-space:nowrap"
title="${compat?'':'⚠ 비호환 예제 — 열면 AI가 자동으로 변환해드립니다'}">
${compat?'열기':'열기 ⚠'}</button>
</div>`).join('');
const hasEx = l.examples?.length > 0;
return `<div style="padding:8px 10px;border-bottom:1px solid var(--line)">
return `<div style="padding:8px 10px;border-bottom:1px solid var(--line)${!compat?';border-left:2px solid #7f1d1d':''}">
<div style="display:flex;align-items:flex-start;gap:6px">
<div style="flex:1;min-width:0">
<div style="font-size:11px;font-weight:700;color:var(--text)">${_csbEsc(l.name)}</div>
<div style="display:flex;align-items:center;gap:5px;flex-wrap:wrap">
<span style="font-size:11px;font-weight:700;color:var(--text)">${_csbEsc(l.name)}</span>
${platformBadge}${_csbArchBadge(archs)}
</div>
<div style="font-size:10px;color:var(--muted);margin-top:1px">${_csbEsc(l.author||'')} · v${_csbEsc(l.version||'?')}${l.category?' · '+_csbEsc(l.category):''}</div>
${l.sentence?`<div style="font-size:10px;color:var(--muted);margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${_csbEsc(l.sentence)}</div>`:''}
${incompatWarn}
</div>
<div style="display:flex;gap:4px;flex-shrink:0">
${hasEx?`<button onclick="document.getElementById('${sid}').style.display=document.getElementById('${sid}').style.display==='none'?'block':'none'"
style="font-size:10px;padding:2px 7px;background:none;border:1px solid var(--line);color:var(--muted);border-radius:4px;cursor:pointer;font-family:inherit">예제</button>`:''}
<button onclick="csbLibUninstall('${_csbEsc(l.name)}')"
style="font-size:10px;padding:2px 7px;background:none;border:1px solid var(--line);color:#f87171;border-radius:4px;cursor:pointer;font-family:inherit">삭제</button>
${isPlatform?'':`<button onclick="csbLibUninstall('${_csbEsc(l.name)}')"
style="font-size:10px;padding:2px 7px;background:none;border:1px solid var(--line);color:#f87171;border-radius:4px;cursor:pointer;font-family:inherit">삭제</button>`}
</div>
</div>
${hasEx?`<div id="${sid}" style="display:none;margin-top:5px;padding:5px 8px;background:var(--panel-2);border-radius:5px">${exRows}</div>`:''}
@@ -4216,15 +4372,25 @@ function _csbLibRenderInstalled(libs) {
}).join('');
}
async function csbLibOpenExample(libName, exName) {
async function csbLibOpenExample(libName, exName, isCompat = true, fqbn = '') {
_csbLibStatus(`예제 로딩: ${exName}...`);
try {
const r = await fetch(`/api/arduino/libs/example?lib=${encodeURIComponent(libName)}&example=${encodeURIComponent(exName)}`, { headers: authHeaders() });
const fqbnParam = (fqbn || _csbSelBoard?.fqbn || '');
const r = await fetch(`/api/arduino/libs/example?lib=${encodeURIComponent(libName)}&example=${encodeURIComponent(exName)}&fqbn=${encodeURIComponent(fqbnParam)}`, { headers: authHeaders() });
const data = await r.json();
if (data.error) { _csbLibStatus('오류: ' + data.error); return; }
_csbLibStatus('');
codeNewFile(data.name, data.code);
setCodeSbTab('files');
if (!isCompat) {
// Auto-send to AI for board adaptation
const hints = _boardSpecificHints(data.code);
const inp = document.getElementById('code-ai-input');
if (inp) {
inp.value = `이 예제는 ${libName} 라이브러리용이지만 현재 보드(${_csbSelBoard.name}, FQBN: ${_csbSelBoard.fqbn})와 호환되지 않을 수 있어.\n${hints}\n\nRead로 ${data.name}을 읽은 뒤 이 보드에서 동작하도록 코드를 수정해줘. 호환되지 않는 API는 이 보드에 맞는 방식으로 교체해.`;
codeAiSend();
}
}
} catch(e) { _csbLibStatus('오류: ' + e.message); }
}
@@ -4246,9 +4412,10 @@ function _csbLibRenderSearch(libs) {
el.innerHTML = libs.map(l => `
<div style="padding:8px 10px;border-bottom:1px solid var(--line);display:flex;align-items:flex-start;gap:6px">
<div style="flex:1;min-width:0">
<div style="display:flex;align-items:center;gap:5px">
<div style="display:flex;align-items:center;gap:5px;flex-wrap:wrap">
<span style="font-size:11px;font-weight:700;color:var(--text)">${_csbEsc(l.name)}</span>
${l.category?`<span style="font-size:9px;padding:1px 5px;border:1px solid var(--line);border-radius:3px;color:var(--muted)">${_csbEsc(l.category)}</span>`:''}
${_csbArchBadge(l.architectures||[])}
</div>
<div style="font-size:10px;color:var(--muted);margin-top:1px">${_csbEsc(l.author||'')} · v${_csbEsc(l.version||'?')}</div>
${l.sentence?`<div style="font-size:10px;color:var(--muted);margin-top:2px">${_csbEsc(l.sentence)}</div>`:''}
@@ -5027,7 +5194,7 @@ function _applyHideBlankLines() {
if (l.includes(blockEnd)) inBlock = false;
} else if (l === '') {
hide = true;
} else if (/^(\/\/|#|--|;)/.test(l)) {
} else if (/^(\/\/|--|;)/.test(l) || /^#(?!include|define|ifdef|ifndef|endif|else|elif|pragma|undef|error|warning)/.test(l)) {
hide = true;
} else if (l.startsWith('/*')) {
hide = true;