인버터 옆처럼 USB를 다시 꽂기 어려운 곳에 보드를 설치하면 시리얼 경로가 무용지물이 된다. 코드앱 업로드 패널은 지금까지 USB 전용이었다 — 플래싱을 브라우저에서 Web Serial(esptool-js/STK500/STM32 UART)로 직접 하는 구조라 무선 경로가 없었다. OTA는 브라우저가 할 수 없는 일이다. 페이지에서 보드로 espota 핸드셰이크를 열 방법이 없어서, 이 업로드 경로만 서버를 경유한다. - 백엔드: /api/arduino/upload의 port가 IP면 OTA로 분기 — 컴파일해서 .bin을 만들고 코어에 들어있는 espota.py로 밀어넣는다. espota.py는 arduino-cli가 아니라 ESP 코어에 딸려오고 코어를 올릴 때마다 버전 디렉토리가 바뀌므로 호출 시점에 최신 버전부터 찾는다. 부트로더/파티션 이미지는 걸러내고 애플리케이션 바이너리만 보낸다. ESP32는 3232, ESP8266은 8266 포트. - 암호는 exec가 아니라 execFile로 넘긴다 — 사용자가 넣는 값이라 셸에 닿으면 안 된다. - 프런트: 패널에 📡 OTA 토글과 보드 IP/암호 입력줄 추가. OTA 모드에서는 시리얼 포트를 안 골라도 업로드 버튼이 살아있어야 하므로 disabled 조건을 분리했다. 에러 힌트는 espota.py의 logging.error() 문구를 직접 읽고 맞췄다. 처음엔 "No response from device"를 기준으로 짐작해서 썼는데, 실제로 응답 없는 보드는 **"No Answer"**를 뱉어서 힌트가 한 번도 안 떴다(실측 확인). 안 뜨는 힌트는 없느니만 못하다. 인증 실패/무응답/Listen 실패를 각각 다른 안내로 매핑. 실측: 실제 rems_solar_esp8266.ino로 엔드포인트 호출 → 컴파일 성공, .bin 탐색, espota 실행, 응답 없는 IP에 대해 3단계 확인 순서 힌트까지 정상 출력. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
722 lines
60 KiB
HTML
722 lines
60 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>💻 코드 에디터</title>
|
|
<style>
|
|
@font-face { font-family: 'D2Coding'; src: url('/fonts/D2Coding.ttf') format('truetype'); font-weight: 400; font-style: normal; }
|
|
@font-face { font-family: 'D2Coding'; src: url('/fonts/D2CodingBold.ttf') format('truetype'); font-weight: 700; font-style: normal; }
|
|
</style>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Nanum+Gothic:wght@400;700;800&display=swap" rel="stylesheet">
|
|
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=JetBrains+Mono:wght@400;500;600&family=Fira+Code:wght@400;500;600&display=swap" rel="stylesheet">
|
|
<link rel="icon" type="image/png" sizes="64x64" href="/homeclaw_logo.png">
|
|
<!-- CodeMirror + xterm (UMD libs loaded before Monaco's AMD loader to avoid anonymous define() clashes) -->
|
|
<link rel="stylesheet" href="/vendor/codemirror/lib/codemirror.css">
|
|
<script src="/vendor/codemirror/lib/codemirror.js"></script>
|
|
<link rel="stylesheet" href="/vendor/xterm.css">
|
|
<script src="/vendor/xterm.js"></script>
|
|
<script src="/vendor/addon-fit.js"></script>
|
|
<script src="/vendor/addon-web-links.js"></script>
|
|
<link rel="stylesheet" href="../css/styles.css">
|
|
<!-- Monaco loader (AMD). Hide define.amd first so any stray UMD lib does not call Monaco's anonymous define(). -->
|
|
<script src="/vendor/vs/loader.js"></script>
|
|
<script>
|
|
try {
|
|
var _t = localStorage.getItem('code-page-theme') || localStorage.getItem('homeclaw_theme') || 'dark';
|
|
document.documentElement.setAttribute('data-theme', _t);
|
|
} catch (e) {}
|
|
</script>
|
|
<style>
|
|
html, body { height: 100%; }
|
|
:root[data-theme="warm"] {
|
|
--bg:#f5f0e8;--bg-soft:#ede8dc;--panel:#fffdf7;--panel-2:#ede8dc;
|
|
--line:#d6ccbb;--text:#2c2318;--muted:#7a6e5e;--brand:#7c5c2e;--brand2:#a07840;
|
|
}
|
|
|
|
/* ── Light/Warm 테마: 하드코딩 다크 그린 오버라이드 ─────────────────── */
|
|
:root:not([data-theme="dark"]) .csb-tab-bar {
|
|
background: var(--panel-2); border-bottom: 1px solid var(--line);
|
|
}
|
|
:root:not([data-theme="dark"]) .code-sb-tab { color: var(--muted); }
|
|
:root:not([data-theme="dark"]) .code-sb-tab.active {
|
|
color: var(--brand); border-bottom-color: var(--brand); background: rgba(0,0,0,.04);
|
|
}
|
|
:root:not([data-theme="dark"]) .code-sb-tab:hover:not(.active) {
|
|
color: var(--text); background: rgba(0,0,0,.04);
|
|
}
|
|
/* 모델 카드 */
|
|
:root:not([data-theme="dark"]) .csb-model-card {
|
|
background: var(--panel); border-color: var(--line);
|
|
}
|
|
:root:not([data-theme="dark"]) .csb-model-card:focus-within {
|
|
border-color: var(--brand); box-shadow: 0 0 0 2px rgba(22,104,227,.1);
|
|
}
|
|
:root:not([data-theme="dark"]) .csb-model-label { color: var(--brand); }
|
|
:root:not([data-theme="dark"]) .csb-model-dot { background: var(--brand); box-shadow: 0 0 5px var(--brand); }
|
|
:root:not([data-theme="dark"]) .csb-model-select-wrap::after { color: var(--brand); }
|
|
:root:not([data-theme="dark"]) .csb-model-select-wrap select { color: var(--text); }
|
|
/* 새 세션 버튼 */
|
|
:root:not([data-theme="dark"]) .csb-new-session-btn {
|
|
background: var(--panel); border-color: var(--line); color: var(--muted);
|
|
}
|
|
:root:not([data-theme="dark"]) .csb-new-session-btn:hover {
|
|
border-color: var(--brand); color: var(--brand); box-shadow: 0 0 0 2px rgba(22,104,227,.1);
|
|
}
|
|
/* 터미널 버튼 */
|
|
:root:not([data-theme="dark"]) .csb-term-btn {
|
|
background: var(--panel); border-color: var(--line); color: var(--muted);
|
|
}
|
|
:root:not([data-theme="dark"]) .csb-term-btn:hover {
|
|
border-color: var(--brand); color: var(--brand); box-shadow: 0 0 0 2px rgba(22,104,227,.1);
|
|
}
|
|
:root:not([data-theme="dark"]) .csb-term-btn.active {
|
|
border-color: var(--brand); color: var(--brand); box-shadow: 0 0 0 2px rgba(22,104,227,.12);
|
|
}
|
|
/* AI 채팅 패널 */
|
|
:root:not([data-theme="dark"]) #code-ai-panel {
|
|
background: var(--bg); border-left-color: var(--line);
|
|
}
|
|
:root:not([data-theme="dark"]) .code-ai-header {
|
|
background: var(--panel-2); border-bottom-color: var(--line);
|
|
}
|
|
:root:not([data-theme="dark"]) #code-ai-title {
|
|
color: var(--brand); text-shadow: none;
|
|
}
|
|
:root:not([data-theme="dark"]) #code-ai-messages { background: var(--bg); }
|
|
/* 메시지 버블 */
|
|
:root:not([data-theme="dark"]) .code-ai-msg.user {
|
|
background: var(--brand); border-color: transparent; color: #fff;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,.12);
|
|
}
|
|
:root:not([data-theme="dark"]) .code-ai-msg.assistant {
|
|
background: var(--panel); border-color: var(--line); color: var(--text);
|
|
box-shadow: 0 1px 4px rgba(0,0,0,.06);
|
|
}
|
|
:root:not([data-theme="dark"]) .code-ai-msg.thinking {
|
|
background: var(--panel-2); border-color: var(--line); color: var(--muted);
|
|
animation: none;
|
|
}
|
|
:root:not([data-theme="dark"]) .code-ai-msg.system { color: var(--muted); }
|
|
/* tech 버튼 */
|
|
:root:not([data-theme="dark"]) .code-ai-tech-btn {
|
|
background: var(--panel); border-color: var(--line); color: var(--muted);
|
|
}
|
|
:root:not([data-theme="dark"]) .code-ai-tech-btn.active {
|
|
border-color: var(--brand); color: var(--brand); background: rgba(22,104,227,.08);
|
|
box-shadow: 0 0 0 1px rgba(22,104,227,.15);
|
|
}
|
|
:root:not([data-theme="dark"]) .code-ai-tech-btn:hover:not(.active) {
|
|
border-color: var(--brand); color: var(--brand);
|
|
}
|
|
/* 입력창 */
|
|
:root:not([data-theme="dark"]) .code-ai-input-row {
|
|
background: var(--panel); border-top-color: var(--line);
|
|
}
|
|
:root:not([data-theme="dark"]) #code-ai-input {
|
|
background: var(--bg); border-color: var(--line); color: var(--text);
|
|
}
|
|
:root:not([data-theme="dark"]) #code-ai-input::placeholder { color: var(--muted); }
|
|
:root:not([data-theme="dark"]) #code-ai-input:focus {
|
|
border-color: var(--brand); box-shadow: 0 0 0 3px rgba(22,104,227,.12);
|
|
}
|
|
/* 세션 바 */
|
|
:root:not([data-theme="dark"]) #code-ai-session-bar {
|
|
background: var(--panel-2); border-top-color: var(--line);
|
|
}
|
|
|
|
/* 미리보기 패널 */
|
|
:root:not([data-theme="dark"]) #code-preview-panel {
|
|
background: var(--bg); border-top-color: var(--line-strong);
|
|
}
|
|
:root:not([data-theme="dark"]) #code-preview-panel > div:first-of-type {
|
|
background: var(--panel-2); border-bottom-color: var(--line);
|
|
}
|
|
:root:not([data-theme="dark"]) #code-preview-url {
|
|
background: var(--bg); border-color: var(--line); color: var(--text);
|
|
}
|
|
:root:not([data-theme="dark"]) #code-preview-url:focus {
|
|
border-color: var(--brand); box-shadow: 0 0 0 2px rgba(22,104,227,.12);
|
|
}
|
|
|
|
/* ── 웜 테마: brand가 amber이므로 rgba 조정 ─────────────────────────── */
|
|
:root[data-theme="warm"] .csb-model-card:focus-within,
|
|
:root[data-theme="warm"] .csb-project-card:focus-within { box-shadow: 0 0 0 2px rgba(124,92,46,.12); }
|
|
:root[data-theme="warm"] .csb-new-session-btn:hover,
|
|
:root[data-theme="warm"] .csb-term-btn:hover,
|
|
:root[data-theme="warm"] .csb-term-btn.active { box-shadow: 0 0 0 2px rgba(124,92,46,.12); }
|
|
:root[data-theme="warm"] .code-ai-msg.user { background: var(--brand); }
|
|
:root[data-theme="warm"] .code-ai-tech-btn.active {
|
|
background: rgba(124,92,46,.08); box-shadow: 0 0 0 1px rgba(124,92,46,.15);
|
|
}
|
|
:root[data-theme="warm"] #code-ai-input:focus { box-shadow: 0 0 0 3px rgba(124,92,46,.12); }
|
|
/* AI 패널 색상 — 다크 기본값, 라이트/웜 오버라이드 */
|
|
:root { --ai-hdr-bg:#0d1f1a; --ai-bdr:#1a3a28; --ai-sess-bg:#0a1812; }
|
|
:root:not([data-theme="dark"]) { --ai-hdr-bg:var(--panel-2); --ai-bdr:var(--line); --ai-sess-bg:var(--bg); }
|
|
.code-theme-btn{background:none;border:1.5px solid var(--line);border-radius:6px;padding:3px 9px;font-size:12px;color:var(--muted);cursor:pointer;transition:.15s;white-space:nowrap;}
|
|
.code-theme-btn.active{border-color:var(--brand);color:var(--brand2);}
|
|
.code-theme-btn:hover{border-color:var(--brand);color:var(--brand2);}
|
|
.code-theme-groups{display:flex;flex-direction:column;gap:5px;margin-top:5px;}
|
|
.code-theme-row{display:flex;align-items:center;gap:5px;}
|
|
.code-theme-row-lbl{font-size:12px;width:52px;flex-shrink:0;color:var(--text);}
|
|
.code-skin-sw{width:20px;height:20px;border-radius:4px;cursor:pointer;border:2px solid var(--line);transition:.15s;box-sizing:border-box;flex-shrink:0;}
|
|
.code-skin-sw.active{border-color:var(--brand) !important;box-shadow:0 0 0 2px var(--brand);transform:scale(1.15);}
|
|
.code-skin-sw:hover{transform:scale(1.1);}
|
|
body { margin: 0; padding: 0; gap: 0; background: var(--bg); color: var(--text); font-family: 'Nanum Gothic', system-ui, sans-serif; display: flex; flex-direction: column; overflow: hidden; height: 100dvh; }
|
|
.code-page-header { display: flex; align-items: center; gap: 10px; padding: 8px 14px; background: var(--panel); border-bottom: 1px solid var(--line); flex-shrink: 0; }
|
|
.code-page-header h1 { font-size: 14px; font-weight: 700; margin: 0; }
|
|
.code-page-header .sub { font-size: 11px; color: var(--muted); }
|
|
.code-layout { flex: 1; display: flex; min-height: 0; }
|
|
.code-sidebar-wrap { width: 240px; flex-shrink: 0; display: flex; flex-direction: column; border-right: 1px solid var(--line); background: var(--panel); min-height: 0; }
|
|
/* Override the main-app inline display:none / flex on the shared sidebar markup */
|
|
#sidebar-code { display: flex !important; }
|
|
#code-view { flex: 1; min-width: 0; }
|
|
|
|
/* ── Mobile ── */
|
|
@media(max-width:1024px){
|
|
.code-page-header{flex-wrap:wrap;padding:6px 10px;gap:5px;}
|
|
.code-page-header h1{font-size:13px;}
|
|
.code-page-header > div:last-child{display:none;}
|
|
/* 좌측 파일 패널 → 왼쪽 오버레이 */
|
|
.code-sidebar-wrap{
|
|
position:fixed;top:0;left:0;bottom:52px;z-index:500;
|
|
width:82vw;max-width:300px;
|
|
transform:translateX(-100%);transition:transform .25s ease;
|
|
box-shadow:4px 0 24px rgba(0,0,0,.4);
|
|
}
|
|
.code-sidebar-wrap.mob-open{transform:translateX(0);}
|
|
/* 우측 AI 패널 → 오른쪽 오버레이 */
|
|
#code-ai-panel{
|
|
position:fixed !important;top:0;right:0;bottom:52px;z-index:500;
|
|
width:92vw;max-width:400px;
|
|
transform:translateX(100%);transition:transform .25s ease;
|
|
box-shadow:-4px 0 28px rgba(0,0,0,.5);
|
|
grid-area:unset !important;
|
|
}
|
|
#code-ai-panel.mob-open{transform:translateX(0);}
|
|
/* 에디터뷰 grid를 에디터 단독으로 */
|
|
#code-view.active{
|
|
grid-template-columns:1fr !important;
|
|
grid-template-rows:1fr auto !important;
|
|
grid-template-areas:"editor" "term" !important;
|
|
}
|
|
.code-vresize{display:none;}
|
|
#mob-backdrop{display:none;position:fixed;inset:0 0 52px 0;background:rgba(0,0,0,.55);z-index:499;}
|
|
#mob-backdrop.show{display:block;}
|
|
#mob-tabbar{display:flex !important;position:fixed;bottom:0;left:0;right:0;z-index:600;background:var(--panel);border-top:1px solid var(--line);height:52px;}
|
|
.code-layout{padding-bottom:52px;}
|
|
input,select,textarea{font-size:16px !important;}
|
|
}
|
|
@media(min-width:1025px){
|
|
#mob-backdrop{display:none !important;}
|
|
#mob-tabbar{display:none !important;}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div id="mob-backdrop" onclick="mobCloseAll()"></div>
|
|
<div id="mob-tabbar" style="display:none">
|
|
<button class="mob-tab" id="mobtab-files" onclick="mobToggle('files')"><span style="font-size:18px">📁</span>파일</button>
|
|
<button class="mob-tab active" id="mobtab-editor" onclick="mobCloseAll()"><span style="font-size:18px">💻</span>에디터</button>
|
|
<button class="mob-tab" id="mobtab-ai" onclick="mobToggle('ai')"><span style="font-size:18px">🤖</span>AI</button>
|
|
</div>
|
|
|
|
<div class="code-page-header">
|
|
<span style="font-size:16px">💻</span>
|
|
<h1>코드 에디터</h1>
|
|
<span class="sub" id="code-header-dir">폴더 없음</span>
|
|
<div style="display:flex;gap:4px;margin-left:8px">
|
|
<button class="code-theme-btn" id="ctbtn-dark" onclick="setCodePageTheme('dark')">🌙 다크</button>
|
|
<button class="code-theme-btn" id="ctbtn-light" onclick="setCodePageTheme('light')">☀️ 라이트</button>
|
|
<button class="code-theme-btn" id="ctbtn-warm" onclick="setCodePageTheme('warm')">🌿 웜</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="code-layout">
|
|
|
|
<!-- CODE SIDEBAR (mirrors #sidebar-code in index.html) -->
|
|
<div class="code-sidebar-wrap">
|
|
<div id="sidebar-code" style="display:flex;flex-direction:column;flex:1;min-height:0;overflow:hidden">
|
|
<!-- 상단 탭: 파일 | 설정 -->
|
|
<div class="csb-tab-bar">
|
|
<button class="code-sb-tab active" id="csb-tab-files" onclick="setCodeSbTab('files')">📄 파일</button>
|
|
<button class="code-sb-tab" id="csb-tab-libraries" onclick="setCodeSbTab('libraries')">아두이노</button>
|
|
<button class="code-sb-tab" id="csb-tab-settings" onclick="setCodeSbTab('settings')">⚙️</button>
|
|
</div>
|
|
|
|
<!-- 파일 탭: 상하 분할 -->
|
|
<div id="csb-files" style="display:flex;flex-direction:column;flex:1;min-height:0">
|
|
<!-- 상단: 파일 트리 -->
|
|
<div style="display:flex;flex-direction:column;flex:1;min-height:0;border-bottom:1px solid var(--line)">
|
|
<div style="display:flex;align-items:center;justify-content:space-between;padding:7px 10px;border-bottom:1px solid var(--line);flex-shrink:0">
|
|
<span id="csb-folder-label" style="font-size:10px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em">폴더 없음</span>
|
|
<div style="display:flex;gap:4px">
|
|
<button id="code-local-btn" onclick="openLocalFolder()" title="로컬 폴더 열기" style="background:none;border:none;cursor:pointer;color:var(--muted);font-size:14px;line-height:1;padding:0 3px">🖥️</button>
|
|
<button onclick="codeSaveFile()" title="파일 저장 (Ctrl+S)" style="background:none;border:none;cursor:pointer;color:var(--muted);font-size:13px;line-height:1;padding:0 3px">💾</button>
|
|
<button onclick="codeNewFile()" title="새 파일" style="background:none;border:none;cursor:pointer;color:var(--muted);font-size:16px;line-height:1;padding:0 3px">+</button>
|
|
<button onclick="csbNewFolderRoot()" title="새 폴더" style="background:none;border:none;cursor:pointer;color:var(--muted);font-size:13px;line-height:1;padding:0 3px">📁</button>
|
|
<button onclick="codeSbRefresh()" title="새로고침" style="background:none;border:none;cursor:pointer;color:var(--muted);font-size:13px;line-height:1;padding:0 3px">↺</button>
|
|
</div>
|
|
</div>
|
|
<!-- 프로젝트 선택 -->
|
|
<div id="csb-project-row" style="display:none;align-items:center;gap:5px;padding:5px 8px;border-bottom:1px solid var(--line);flex-shrink:0;background:var(--panel-2)">
|
|
<span style="font-size:11px;flex-shrink:0">📁</span>
|
|
<select id="csb-project-sel" onchange="switchProject(this.value||null)"
|
|
style="flex:1;font-size:11px;padding:3px 5px;background:var(--panel);color:var(--text);border:1px solid var(--line);border-radius:5px;min-width:0;cursor:pointer">
|
|
</select>
|
|
<button onclick="deleteActiveProject()" title="현재 프로젝트 삭제" style="flex-shrink:0;background:none;border:none;color:var(--muted);cursor:pointer;font-size:13px;padding:2px 4px;border-radius:4px;line-height:1" onmouseover="this.style.color='#f87171'" onmouseout="this.style.color='var(--muted)'">🗑</button>
|
|
</div>
|
|
<div id="csb-file-tree" style="flex:1;overflow-y:auto;padding:4px 0">
|
|
<div style="padding:12px;color:var(--muted);font-size:11px;text-align:center">로딩 중...</div>
|
|
</div>
|
|
<!-- 보기 옵션 -->
|
|
<div style="display:flex;gap:4px;padding:5px 8px;flex-shrink:0;border-top:1px solid var(--line)">
|
|
<button id="code-opt-hideblank" class="csb-term-btn" onclick="csbHideBlankLines(!_hideBlankLines)" title="빈 줄·주석 줄 숨기기" style="font-size:11px;padding:4px 0">¶ 빈줄숨김</button>
|
|
<button id="code-opt-blockff" class="csb-term-btn" onclick="csbBlockFlipFlop(!_blockFlipFlop)" title="함수/블록 교대 배경색" style="font-size:11px;padding:4px 0">▓ 블록색상</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 하단: 모델 설정 패널 -->
|
|
<div style="flex-shrink:0;display:flex;flex-direction:column;overflow:hidden;border-top:1px solid var(--line)">
|
|
<!-- 모델 선택 -->
|
|
<div style="display:flex;flex-direction:column;flex:1;overflow-y:auto;padding:10px 12px;gap:8px">
|
|
<button onclick="codeAiClear()" class="csb-new-session-btn">↺ 새 세션</button>
|
|
<!-- 보드 선택 -->
|
|
<div style="display:flex;flex-direction:column;gap:3px">
|
|
<div style="font-size:10px;color:var(--muted);font-weight:600;letter-spacing:.04em">컴파일 보드</div>
|
|
<div style="position:relative">
|
|
<div id="csb-board-sel-display" onclick="csbBoardSelOpen()"
|
|
style="display:flex;align-items:center;gap:6px;padding:6px 10px;background:var(--panel-2);border:1px solid var(--line);border-radius:6px;cursor:pointer;font-size:11px;min-height:30px">
|
|
<span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text);font-weight:600" id="csb-board-sel-name">Arduino UNO</span>
|
|
<span id="csb-board-qemu-badge" style="display:none;font-size:9px;padding:1px 5px;border-radius:3px;flex-shrink:0;background:#1a3a1a;color:#22c55e;border:1px solid #2a5a2a">🖥 QEMU</span>
|
|
<span style="color:var(--muted);flex-shrink:0;font-size:10px">▾</span>
|
|
</div>
|
|
<!-- position:fixed + JS로 좌표 계산(csbBoardSelOpen) — 부모 패널이 overflow:hidden/auto라
|
|
position:absolute로는 그 경계 밖으로 못 나가고 잘림("2칸만 보임" 리포트, 2026-07-21) -->
|
|
<div id="csb-board-sel-dropdown" style="display:none;position:fixed;z-index:200;background:var(--panel);border:1px solid var(--line);border-left:3px solid var(--brand);border-radius:6px;box-shadow:0 -8px 28px rgba(0,0,0,.22);overflow:hidden;flex-direction:column">
|
|
<div style="padding:6px 8px;border-bottom:1px solid var(--line);flex-shrink:0">
|
|
<input id="csb-board-sel-q" placeholder="보드 검색..." oninput="csbBoardSelSearch()" onkeydown="csbBoardSelKey(event)"
|
|
style="width:100%;font-size:11px;padding:5px 8px;background:var(--bg);color:var(--text);border:1px solid var(--line);border-radius:4px;font-family:inherit;box-sizing:border-box;outline:none">
|
|
</div>
|
|
<div id="csb-board-sel-list" style="overflow-y:auto;flex:1;max-height:min(60vh,560px);display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));align-content:start"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- 실행 + AI 전송 -->
|
|
<div style="display:flex;gap:6px;margin-top:2px">
|
|
<div id="code-run-dropdown" style="position:relative;display:inline-flex;align-items:center;flex:1">
|
|
<button class="code-tool-btn btn-run" onclick="codeRunDefault()" title="실행" style="flex:1;justify-content:center;display:flex;align-items:center;gap:4px;padding:6px 0">
|
|
<span id="code-run-icon">▶</span> <span id="code-run-label">실행</span>
|
|
</button>
|
|
<button class="code-tool-btn btn-run" onclick="codeToggleRunMenu()" title="실행 방법 선택" style="padding:6px 4px;margin-left:-1px;border-left:1px solid rgba(34,197,94,.3)">▾</button>
|
|
<div id="code-run-menu" style="display:none;position:absolute;top:100%;left:0;z-index:99;min-width:180px;background:#1a2e1a;border:1px solid #2d5a3a;border-radius:6px;box-shadow:0 4px 16px rgba(0,0,0,.5);margin-top:2px;overflow:hidden"></div>
|
|
</div>
|
|
<button class="code-tool-btn btn-ai" onclick="codeSendSelectionToAI()" title="선택 영역을 AI에게 전송" style="flex:1;justify-content:center;padding:6px 0">💬 전송</button>
|
|
</div>
|
|
<!-- 터미널 + companion -->
|
|
<div class="csb-term-row">
|
|
<button id="code-term-toggle-btn" class="csb-term-btn" onclick="toggleCodeTerminal()" title="터미널 토글 (Ctrl+`)">⌨ 터미널</button>
|
|
<span id="companion-dot" onclick="companionConnect()" title="Python companion 미연결 — 클릭해서 재연결"
|
|
style="width:10px;height:10px;border-radius:50%;background:#6b7280;display:inline-block;cursor:pointer;flex-shrink:0"></span>
|
|
<a href="/companion.py" download title="companion.py 다운로드 (Bash 실행용 로컬 클라이언트, Windows/macOS/Linux 공용 — 업데이트 후에는 재다운로드 필요)" style="color:var(--muted);font-size:12px;text-decoration:none;flex-shrink:0">⬇</a>
|
|
<a href="/install_autostart.py" download title="install_autostart.py 다운로드 (Windows 전용) — 이 파일 하나만 실행하면 최신 companion.py를 %LOCALAPPDATA%\PythonCompanion에 설치하고 Windows 로그인 시 자동 실행되도록 등록합니다. 다시 실행하면 최신 버전으로 업데이트됩니다" style="color:var(--muted);font-size:12px;text-decoration:none;flex-shrink:0">⚙</a>
|
|
</div>
|
|
<!-- 미리보기 -->
|
|
<div class="csb-term-row">
|
|
<button id="code-preview-toggle-btn" class="csb-term-btn" onclick="toggleCodePreview()" title="브라우저 미리보기 패널">🌐 미리보기</button>
|
|
</div>
|
|
<!-- Arduino 에뮬레이터 + 업로드 -->
|
|
<div class="csb-term-row">
|
|
<button id="code-emu-btn" class="csb-term-btn" onclick="openArduinoEmulator()" title="Arduino 에뮬레이터 열기">아두이노</button>
|
|
<button id="code-compile-btn" class="csb-term-btn" onclick="csbCompileOnly()" title="실제 보드용 컴파일 (오류 시 AI 채팅으로 전송)">🔨 컴파일</button>
|
|
<button id="code-upload-btn" class="csb-term-btn" onclick="csbUploadToBoard()" title="보드에 업로드 (USB 연결 필요)" style="background:var(--brand);color:#fff;border-color:var(--brand)">⚡ 업로드</button>
|
|
<button id="code-android-btn" class="csb-term-btn" onclick="openAndroidEmulator()" title="Android 에뮬레이터 열기">🤖 Android</button>
|
|
<button id="code-android-reset-btn" class="csb-term-btn" onclick="resetAndroidEmulator()" title="Android 에뮬레이터 세션이 꼬였을 때 강제 종료 후 재시작">🔄</button>
|
|
</div>
|
|
<div class="csb-term-row">
|
|
<button id="code-usb-android-btn" class="csb-term-btn" onclick="toggleAndroidUsbPanel()" title="USB로 연결된 실제 Android 기기 (기기 감지 / APK 설치 / Logcat / 화면 미러링)">📱 실기기(USB)</button>
|
|
</div>
|
|
<!-- Claude App (로컬 LLM 코딩 에이전트) -->
|
|
<div class="csb-term-row">
|
|
<button id="code-claude-btn" class="csb-term-btn" onclick="toggleClaudeAppPanel()" title="Claude App(로컬 LLM 코딩 에이전트) 패널 토글">🤖 클로드 앱</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 라이브러리/보드 탭 -->
|
|
<div id="csb-libraries" style="display:none;flex-direction:column;flex:1;min-height:0">
|
|
<!-- 상단 섹션 탭: 라이브러리 | 보드 -->
|
|
<div style="display:flex;border-bottom:2px solid var(--line);flex-shrink:0;background:var(--panel-2)">
|
|
<button class="code-sb-tab active" id="csb-ardsec-lib" onclick="csbArdSection('lib')" style="flex:1;font-size:11px;padding:7px 0">📦 라이브러리</button>
|
|
<button class="code-sb-tab" id="csb-ardsec-board" onclick="csbArdSection('board')" style="flex:1;font-size:11px;padding:7px 0">🔌 보드</button>
|
|
</div>
|
|
|
|
<!-- ── 라이브러리 패널 ── -->
|
|
<div id="csb-ard-lib" style="display:flex;flex-direction:column;flex:1;min-height:0">
|
|
<div style="display:flex;border-bottom:1px solid var(--line);flex-shrink:0">
|
|
<button class="code-sb-tab active" id="csb-libtab-installed" onclick="csbLibTab('installed')" style="flex:1;font-size:10px;padding:5px 0">설치됨</button>
|
|
<button class="code-sb-tab" id="csb-libtab-search" onclick="csbLibTab('search')" style="flex:1;font-size:10px;padding:5px 0">검색/설치</button>
|
|
</div>
|
|
<div id="csb-lib-filter-row" style="display:flex;flex-direction:column;padding:4px 6px;gap:3px;border-bottom:1px solid var(--line);flex-shrink:0">
|
|
<div style="display:flex;align-items:center;gap:4px;flex-wrap:wrap">
|
|
<span style="font-size:9px;color:var(--muted)">보기:</span>
|
|
<button id="csb-libv-flat" onclick="csbLibView('flat')" style="font-size:9px;padding:2px 7px;border-radius:3px;border:1px solid var(--brand);background:var(--brand);color:#fff;cursor:pointer;font-family:inherit">목록</button>
|
|
<button id="csb-libv-category" onclick="csbLibView('category')" style="font-size:9px;padding:2px 7px;border-radius:3px;border:1px solid var(--line);background:var(--panel-2);color:var(--muted);cursor:pointer;font-family:inherit">종류별</button>
|
|
<span style="font-size:9px;color:var(--muted);margin-left:4px">보드:</span>
|
|
<button id="csb-libf-all" onclick="csbLibFilter('all')" style="font-size:9px;padding:2px 7px;border-radius:3px;border:1px solid var(--brand);background:var(--brand);color:#fff;cursor:pointer;font-family:inherit">전체</button>
|
|
<button id="csb-libf-compat" onclick="csbLibFilter('compat')" style="font-size:9px;padding:2px 7px;border-radius:3px;border:1px solid var(--line);background:var(--panel-2);color:var(--muted);cursor:pointer;font-family:inherit">이 보드만</button>
|
|
<button id="csb-libf-incompat" onclick="csbLibFilter('incompat')" style="font-size:9px;padding:2px 7px;border-radius:3px;border:1px solid var(--line);background:var(--panel-2);color:var(--muted);cursor:pointer;font-family:inherit">비호환</button>
|
|
</div>
|
|
<div id="csb-lib-arch-row" style="display:none;flex-wrap:wrap;gap:3px"></div>
|
|
</div>
|
|
<div id="csb-lib-search-row" style="display:none;padding:6px 8px;border-bottom:1px solid var(--line);gap:5px;flex-shrink:0;align-items:center">
|
|
<input id="csb-lib-q" placeholder="키워드 또는 라이브러리명..." onkeydown="if(event.key==='Enter')csbLibSearch()"
|
|
style="flex:1;font-size:11px;padding:5px 8px;background:var(--panel-2);color:var(--text);border:1px solid var(--line);border-radius:6px;font-family:inherit;min-width:0">
|
|
<button onclick="csbLibSearch()" class="csb-term-btn" style="padding:5px 8px;font-size:11px;flex-shrink:0">검색</button>
|
|
</div>
|
|
<div id="csb-lib-list" style="flex:1;overflow-y:auto;padding:4px 0">
|
|
<div style="padding:16px 12px;color:var(--muted);font-size:11px;text-align:center">불러오는 중...</div>
|
|
</div>
|
|
<div id="csb-lib-status" style="padding:5px 10px;font-size:10px;color:var(--brand);border-top:1px solid var(--line);min-height:24px;flex-shrink:0"></div>
|
|
</div>
|
|
|
|
<!-- ── 보드 매니저 패널 ── -->
|
|
<div id="csb-ard-board" style="display:none;flex-direction:column;flex:1;min-height:0">
|
|
<div style="display:flex;border-bottom:1px solid var(--line);flex-shrink:0">
|
|
<button class="code-sb-tab active" id="csb-boardtab-installed" onclick="csbBoardTab('installed')" style="flex:1;font-size:10px;padding:5px 0">설치됨</button>
|
|
<button class="code-sb-tab" id="csb-boardtab-search" onclick="csbBoardTab('search')" style="flex:1;font-size:10px;padding:5px 0">검색/설치</button>
|
|
</div>
|
|
<div id="csb-board-search-row" style="display:none;padding:6px 8px;border-bottom:1px solid var(--line);gap:5px;flex-shrink:0;align-items:center">
|
|
<input id="csb-board-q" placeholder="보드 이름 (예: esp32, rp2040...)" onkeydown="if(event.key==='Enter')csbBoardSearch()"
|
|
style="flex:1;font-size:11px;padding:5px 8px;background:var(--panel-2);color:var(--text);border:1px solid var(--line);border-radius:6px;font-family:inherit;min-width:0">
|
|
<button onclick="csbBoardSearch()" class="csb-term-btn" style="padding:5px 8px;font-size:11px;flex-shrink:0">검색</button>
|
|
</div>
|
|
<div id="csb-board-list" style="flex:1;overflow-y:auto;padding:4px 0">
|
|
<div style="padding:16px 12px;color:var(--muted);font-size:11px;text-align:center">불러오는 중...</div>
|
|
</div>
|
|
<div id="csb-board-status" style="padding:5px 10px;font-size:10px;color:var(--brand);border-top:1px solid var(--line);min-height:24px;flex-shrink:0"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 설정 탭 -->
|
|
<div id="csb-settings" style="display:none;flex-direction:column;flex:1;min-height:0;overflow-y:auto;padding:14px 12px;gap:14px">
|
|
<div>
|
|
<div style="font-size:10px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:6px">터미널 시작 경로</div>
|
|
<div style="display:flex;align-items:center;gap:6px">
|
|
<input type="text" id="csb-termpath" placeholder="/home/kim/projects/mygame"
|
|
style="flex:1;font-size:12px;padding:6px 8px;background:var(--panel-2);color:var(--text);border:1px solid var(--line);border-radius:8px;font-family:'IBM Plex Mono',monospace"
|
|
onkeydown="if(event.key==='Enter')csbTermPathSave()">
|
|
<button onclick="csbTermPathSave()" style="background:var(--brand);color:#fff;border:none;border-radius:8px;padding:6px 10px;font-size:11px;font-weight:700;cursor:pointer;white-space:nowrap">저장</button>
|
|
</div>
|
|
<div id="csb-termpath-status" style="font-size:10px;color:var(--muted);margin-top:4px">새 터미널 탭 열면 이 경로로 자동 이동</div>
|
|
</div>
|
|
<div>
|
|
<div style="font-size:10px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:6px">에디터 폰트</div>
|
|
<select id="csb-fontfamily" onchange="csbFontFamily(this.value)"
|
|
style="width:100%;font-size:12px;padding:6px 8px;background:var(--panel-2);color:var(--text);border:1px solid var(--line);border-radius:8px;cursor:pointer">
|
|
<option value="D2Coding">D2Coding</option>
|
|
<option value="JetBrains Mono">JetBrains Mono</option>
|
|
<option value="IBM Plex Mono">IBM Plex Mono</option>
|
|
<option value="Fira Code">Fira Code</option>
|
|
<option value="Nanum Gothic">Nanum Gothic</option>
|
|
<option value="monospace">시스템 기본</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<div style="font-size:10px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:6px">폰트 웨이트</div>
|
|
<select id="csb-fontweight" onchange="csbFontWeight(this.value)"
|
|
style="width:100%;font-size:12px;padding:6px 8px;background:var(--panel-2);color:var(--text);border:1px solid var(--line);border-radius:8px;cursor:pointer">
|
|
<option value="300">Light (300)</option>
|
|
<option value="400" selected>Regular (400)</option>
|
|
<option value="500">Medium (500)</option>
|
|
<option value="600">Semi Bold (600)</option>
|
|
<option value="700">Bold (700)</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<div style="font-size:10px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:6px">폰트 크기</div>
|
|
<div style="display:flex;align-items:center;gap:8px">
|
|
<input type="range" id="csb-fontsize" min="10" max="22" value="14" oninput="csbFontSize(this.value)"
|
|
style="flex:1;accent-color:var(--brand)">
|
|
<span id="csb-fontsize-label" style="font-size:11px;color:var(--muted);width:24px;text-align:right">14</span>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div style="font-size:10px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:6px">줄 바꿈</div>
|
|
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;font-size:12px;color:var(--text)">
|
|
<input type="checkbox" id="csb-wordwrap" checked onchange="csbWordWrap(this.checked)"
|
|
style="accent-color:var(--brand);width:15px;height:15px">
|
|
자동 줄 바꿈
|
|
</label>
|
|
</div>
|
|
<div>
|
|
<div style="font-size:10px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:2px">에디터 테마 <span id="csb-theme-lbl" style="font-weight:400;text-transform:none;letter-spacing:0;color:var(--text)"></span></div>
|
|
<div class="code-skin-grid" id="csb-theme-grid"></div>
|
|
</div>
|
|
<div>
|
|
<div style="font-size:10px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:6px">탭 크기</div>
|
|
<select id="csb-tabsize" onchange="csbTabSize(this.value)"
|
|
style="width:100%;font-size:12px;padding:6px 8px;background:var(--panel-2);color:var(--text);border:1px solid var(--line);border-radius:8px;cursor:pointer">
|
|
<option value="2" selected>2칸</option>
|
|
<option value="4">4칸</option>
|
|
<option value="8">8칸</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<div style="font-size:10px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:6px">미니맵</div>
|
|
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;font-size:12px;color:var(--text)">
|
|
<input type="checkbox" id="csb-minimap" checked onchange="csbMinimap(this.checked)"
|
|
style="accent-color:var(--brand);width:15px;height:15px">
|
|
미니맵 표시
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Code Editor View (mirrors #code-view in index.html) ─────────────── -->
|
|
<div id="code-view" class="active">
|
|
<!-- Editor column: tabs + diff + editor -->
|
|
<div class="code-editor-col">
|
|
<div class="code-tabs-bar">
|
|
<div id="code-file-tabs" style="display:flex;flex:1"></div>
|
|
</div>
|
|
<div id="code-diff-bar" style="display:none">
|
|
<span id="code-diff-label">🔀 AI 제안 변경 사항</span>
|
|
<span id="code-diff-hunk-info" style="font-size:11px;color:var(--muted)"></span>
|
|
<button id="code-diff-accept" onclick="acceptAllHunks()">✅ 모두 적용</button>
|
|
<button id="code-diff-reject" onclick="rejectAllHunks()">❌ 모두 취소</button>
|
|
</div>
|
|
<div id="code-editor-container"></div>
|
|
</div>
|
|
<div class="code-vresize" id="code-vresize"></div>
|
|
<!-- AI panel: starts at the very top, side by side with editor -->
|
|
<div id="code-ai-panel">
|
|
<div id="code-ai-panel-header" style="display:flex;align-items:center;padding:4px 8px;border-bottom:1px solid var(--ai-bdr);background:var(--ai-hdr-bg);flex-shrink:0;gap:6px">
|
|
<div style="display:flex;align-items:center;gap:7px;flex:1;min-width:0">
|
|
<span style="font-size:16px;line-height:1;flex-shrink:0">🤖</span>
|
|
<div style="display:flex;flex-direction:column;gap:1px;flex:1;min-width:0">
|
|
<span style="font-size:9px;font-weight:700;color:#22c55e;text-transform:uppercase;letter-spacing:.07em;line-height:1">Coder AI</span>
|
|
<div class="csb-model-select-wrap" style="min-width:0;width:100%">
|
|
<select id="csb-model-select" onchange="csbModelChanged()" style="font-size:12px;font-weight:700;width:100%"></select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<button id="code-opt-karpathy" class="csb-term-btn" onclick="csbKarpathy(!_karpathyMode)" title="Karpathy 코딩 가이드라인 적용" style="font-size:11px;padding:4px 8px;border-radius:4px;flex-shrink:0">🧠 Karpathy</button>
|
|
</div>
|
|
<div id="code-ai-messages">
|
|
<div class="code-ai-msg thinking">코드를 선택하고 "AI에게 전송"을 누르거나, 아래에 질문을 입력하세요.</div>
|
|
</div>
|
|
<div id="code-ai-session-bar" style="display:flex;align-items:center;padding:5px 10px;border-top:1px solid var(--ai-bdr);background:var(--ai-sess-bg);flex-shrink:0;gap:8px">
|
|
<div style="display:flex;align-items:center;gap:5px;min-width:0;flex:1;overflow:hidden">
|
|
<span style="font-size:10px;color:#4a7a5c;font-weight:700;text-transform:uppercase;letter-spacing:.05em;flex-shrink:0">세션</span>
|
|
<span id="code-ai-session-bar-project" style="font-size:11px;color:#86c9a0;font-family:'IBM Plex Mono',monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:600">(전체)</span>
|
|
<span id="code-ai-session-bar-count" style="font-size:10px;color:#4a7a5c;flex-shrink:0"></span>
|
|
</div>
|
|
<button id="code-ai-back-btn" onclick="_backToSessionList()" title="세션 목록으로" style="display:none;flex-shrink:0;background:none;border:1px solid #2a4a38;color:#5a9e78;border-radius:4px;padding:2px 7px;font-size:10px;cursor:pointer;font-family:'IBM Plex Mono',monospace" onmouseover="this.style.borderColor='#22c55e';this.style.color='#22c55e'" onmouseout="this.style.borderColor='#2a4a38';this.style.color='#5a9e78'">← 목록</button>
|
|
<div id="code-ai-ctx-wrap" title="클릭하면 최대값 재확인" onclick="_codeAiCtxMaxFetchedFor='';_fetchModelCtxMax(codeAiModel);" style="display:none;flex-shrink:0;align-items:center;gap:5px;cursor:pointer">
|
|
<span id="code-ai-ctx-label" style="font-size:11px;color:#86c9a0;white-space:nowrap;font-family:'IBM Plex Mono',monospace"></span>
|
|
<div style="width:56px;height:4px;background:#1a3a28;border-radius:4px;overflow:hidden;flex-shrink:0">
|
|
<div id="code-ai-ctx-fill" style="height:100%;width:0%;border-radius:4px;transition:width .4s,background .4s"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="code-ai-img-preview" style="display:none;flex-wrap:wrap;gap:6px;padding:6px 10px 0;"></div>
|
|
<div class="code-ai-input-row" id="code-ai-input-row"
|
|
ondragover="event.preventDefault();this.classList.add('drag-over')"
|
|
ondragleave="this.classList.remove('drag-over')"
|
|
ondrop="_codeAiDropImage(event)">
|
|
<textarea id="code-ai-input" placeholder="AI에게 질문... (이미지 붙여넣기/드롭 가능)" rows="1"
|
|
oninput="this.style.height='auto';this.style.height=Math.min(this.scrollHeight,200)+'px'"
|
|
onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();codeAiSend();}"
|
|
onpaste="_codeAiPasteImage(event)"></textarea>
|
|
<button id="code-ai-send-btn" class="code-tool-btn primary" onclick="codeAiSend()" style="flex-shrink:0;align-self:flex-end">전송</button>
|
|
</div>
|
|
</div>
|
|
<!-- Bottom panels: terminal + preview (grid-area: term) -->
|
|
<div class="code-bottom-panels">
|
|
<!-- Code editor terminal panel -->
|
|
<div id="code-term-panel" style="display:none;flex-direction:column;flex-shrink:0;height:260px;min-height:80px;border-top:2px solid #1a4a35;background:#4B1D45;position:relative">
|
|
<div id="code-term-drag" style="position:absolute;top:0;left:0;right:0;height:5px;cursor:ns-resize;z-index:2;transition:background .15s"></div>
|
|
<div style="display:flex;align-items:center;height:28px;background:#0d1f1a;border-bottom:1px solid #1a4a35;flex-shrink:0;overflow-x:auto;padding-left:2px">
|
|
<div id="code-term-tabs" style="display:flex;align-items:stretch;height:100%;flex:1"></div>
|
|
<button onclick="addLocalShell()" id="local-shell-btn" title="로컬(companion) 탭 추가" style="background:none;border:none;color:#22c55e;cursor:pointer;font-size:16px;padding:0 8px;line-height:1;flex-shrink:0">+</button>
|
|
<button onclick="addCodeTermShell()" title="서버 터미널 탭 추가" style="background:none;border:none;color:#6b7280;cursor:pointer;font-size:11px;padding:0 6px;line-height:1;flex-shrink:0;font-family:'IBM Plex Mono',monospace">☁️</button>
|
|
<button id="code-term-ai-error-btn" onclick="_sendActiveTermErrorToAI()" title="터미널 출력을 AI에게 전송" style="background:#1a3a28;color:#22c55e;border:1px solid #2a5a3a;border-radius:4px;padding:1px 7px;font-size:10px;font-weight:700;cursor:pointer;flex-shrink:0;margin-right:2px">🤖</button>
|
|
<button onclick="codeTermKill()" title="실행 중단 (Ctrl+C)" style="background:none;border:none;color:#f87171;cursor:pointer;font-size:13px;padding:0 6px;line-height:1;flex-shrink:0">⏹</button>
|
|
<button id="code-term-fullscreen-btn" onclick="toggleCodeTermFullscreen()" title="전체화면" style="background:none;border:none;color:#8b8b8b;cursor:pointer;font-size:13px;padding:0 6px;line-height:1;flex-shrink:0">⛶</button>
|
|
<button onclick="toggleCodeTerminal()" title="닫기" style="background:none;border:none;color:#3d6b4f;cursor:pointer;font-size:14px;padding:0 8px;line-height:1;flex-shrink:0">×</button>
|
|
</div>
|
|
<div id="code-term-container" style="flex:1;min-height:0;position:relative;overflow:hidden"></div>
|
|
<div id="code-terminal-panel" style="position:relative;height:0"></div>
|
|
</div>
|
|
<!-- Browser preview panel -->
|
|
<div id="code-preview-panel" style="display:none;flex-direction:column;flex-shrink:0;height:320px;min-height:100px;border-top:2px solid #1a3a5c;background:#080f18;position:relative">
|
|
<div id="code-preview-drag" style="position:absolute;top:0;left:0;right:0;height:5px;cursor:ns-resize;z-index:2;transition:background .15s"></div>
|
|
<div style="display:flex;align-items:center;height:30px;background:#0d1a2a;border-bottom:1px solid #1a3a5c;flex-shrink:0;padding:0 6px;gap:4px">
|
|
<span style="color:#60a5fa;font-size:13px;flex-shrink:0">🌐</span>
|
|
<input id="code-preview-url" type="text" value="http://127.0.0.1:8000"
|
|
style="flex:1;background:#0a1520;border:1px solid #1e3a5c;color:#93c5fd;font-size:11px;font-family:'IBM Plex Mono',monospace;padding:2px 6px;border-radius:3px;outline:none;min-width:0"
|
|
placeholder="http://127.0.0.1:8000"
|
|
onchange="localStorage.setItem('code_preview_url', this.value)"
|
|
onkeydown="if(event.key==='Enter') codePreviewRefresh()">
|
|
<button onclick="codePreviewRefresh()" title="새로고침" style="background:none;border:none;color:#60a5fa;cursor:pointer;font-size:14px;padding:0 4px;flex-shrink:0">↻</button>
|
|
<button onclick="window.open(document.getElementById('code-preview-url').value,'_blank')" title="새 탭에서 열기" style="background:none;border:none;color:#6b7280;cursor:pointer;font-size:12px;padding:0 4px;flex-shrink:0">⧉</button>
|
|
<button onclick="toggleCodePreview()" title="닫기" style="background:none;border:none;color:#3d5a7a;cursor:pointer;font-size:14px;padding:0 4px;flex-shrink:0">×</button>
|
|
</div>
|
|
<iframe id="code-preview-iframe" src="" style="flex:1;border:none;background:#fff;min-height:0" sandbox="allow-scripts allow-same-origin allow-forms allow-modals allow-popups"></iframe>
|
|
</div>
|
|
<!-- 🖥️ Emulator panel -->
|
|
<div id="code-emu-panel" style="display:none;flex-direction:column;flex-shrink:0;height:75vh;min-height:120px;border-top:2px solid #1a4a25;background:#0a1410;position:relative">
|
|
<div id="code-emu-drag" style="position:absolute;top:0;left:0;right:0;height:5px;cursor:ns-resize;z-index:2;transition:background .15s"></div>
|
|
<div style="display:flex;align-items:center;height:32px;background:#0d1f18;border-bottom:1px solid #1a4a25;flex-shrink:0;padding:0 8px;gap:6px">
|
|
<span style="font-size:13px;flex-shrink:0">🖥️</span>
|
|
<span id="emu-panel-title" style="font-size:12px;font-weight:700;color:#4ade80;font-family:'IBM Plex Mono',monospace;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">에뮬레이터</span>
|
|
<button onclick="emuPanelNewTab()" title="새 탭에서 열기" style="background:none;border:none;color:#4a7a5c;cursor:pointer;font-size:13px;padding:0 5px;flex-shrink:0">⧉</button>
|
|
<button onclick="toggleEmuPanel()" title="닫기" style="background:none;border:none;color:#3d6b4f;cursor:pointer;font-size:16px;padding:0 6px;line-height:1;flex-shrink:0">×</button>
|
|
</div>
|
|
<iframe id="code-emu-iframe" src="" style="flex:1;border:none;min-height:0" allow="serial;usb"></iframe>
|
|
</div>
|
|
<!-- 🤖 Claude App panel -->
|
|
<div id="code-claude-panel" style="display:none;flex-direction:column;flex-shrink:0;height:560px;min-height:160px;border-top:2px solid #4a2d6b;background:#140f1c;position:relative">
|
|
<div id="code-claude-drag" style="position:absolute;top:0;left:0;right:0;height:5px;cursor:ns-resize;z-index:2;transition:background .15s"></div>
|
|
<div style="display:flex;align-items:center;height:30px;background:#1c1424;border-bottom:1px solid #4a2d6b;flex-shrink:0;padding:0 8px;gap:6px">
|
|
<span style="font-size:13px;flex-shrink:0">🤖</span>
|
|
<span style="font-size:12px;font-weight:700;color:#c9a9e8;font-family:'IBM Plex Mono',monospace;flex:1">Claude App</span>
|
|
<button onclick="window.open('/html/claude-app.html','_blank')" title="새 탭에서 열기" style="background:none;border:none;color:#8a6c9e;cursor:pointer;font-size:13px;padding:0 5px;flex-shrink:0">⧉</button>
|
|
<button onclick="toggleClaudeAppPanel()" title="닫기" style="background:none;border:none;color:#6b4d7e;cursor:pointer;font-size:16px;padding:0 6px;line-height:1;flex-shrink:0">×</button>
|
|
</div>
|
|
<iframe id="code-claude-iframe" src="" style="flex:1;border:none;min-height:0" allow="local-network-access"></iframe>
|
|
</div>
|
|
<!-- ⚡ Upload panel -->
|
|
<div id="code-upload-panel" style="display:none;flex-direction:column;flex-shrink:0;height:300px;min-height:100px;border-top:2px solid #7a4e20;background:#1e1810;position:relative">
|
|
<div id="code-upload-drag" style="position:absolute;top:0;left:0;right:0;height:5px;cursor:ns-resize;z-index:2;transition:background .15s"></div>
|
|
<div style="display:flex;align-items:center;height:32px;background:#2a1e10;border-bottom:1px solid #5a3820;flex-shrink:0;padding:0 8px;gap:6px;overflow-x:auto">
|
|
<span id="cup-status" style="padding:2px 8px;border-radius:4px;font-size:11px;font-weight:700;background:#3a2810;color:#e09040;flex-shrink:0;white-space:nowrap">대기 중</span>
|
|
<span id="cup-port-name" style="font-size:12px;color:#c8a060;font-family:'IBM Plex Mono',monospace;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1">포트 없음</span>
|
|
<button id="cup-tbtn-log" onclick="cupTab('log')" style="background:none;border:none;color:#e89040;cursor:pointer;font-size:12px;font-family:'IBM Plex Mono',monospace;padding:0 8px;height:100%;border-bottom:2px solid #e89040;white-space:nowrap;flex-shrink:0">OUTPUT</button>
|
|
<button id="cup-tbtn-opts" onclick="cupTab('opts')" style="background:none;border:none;color:#8a7050;cursor:pointer;font-size:12px;font-family:'IBM Plex Mono',monospace;padding:0 8px;height:100%;border-bottom:2px solid transparent;white-space:nowrap;flex-shrink:0">옵션</button>
|
|
<button id="cup-tbtn-mon" onclick="cupTab('mon')" style="background:none;border:none;color:#8a7050;cursor:pointer;font-size:12px;font-family:'IBM Plex Mono',monospace;padding:0 8px;height:100%;border-bottom:2px solid transparent;white-space:nowrap;flex-shrink:0">시리얼</button>
|
|
<button onclick="cupSelectPort()" style="background:#3a2818;border:1px solid #6a4820;color:#e89040;border-radius:5px;padding:3px 10px;font-size:12px;cursor:pointer;font-family:'IBM Plex Mono',monospace;flex-shrink:0;white-space:nowrap">🔌 포트</button>
|
|
<button id="cup-btn-ota" onclick="cupToggleOta()" title="WiFi로 무선 업로드 (ESP8266/ESP32, 보드에 OTA 코드가 이미 올라가 있어야 함)" style="background:#3a2818;border:1px solid #6a4820;color:#e89040;border-radius:5px;padding:3px 10px;font-size:12px;cursor:pointer;font-family:'IBM Plex Mono',monospace;flex-shrink:0;white-space:nowrap">📡 OTA</button>
|
|
<button id="cup-btn-download" onclick="cupDoDownload()" disabled style="background:#3a2818;border:1px solid #4a3820;color:#907050;border-radius:5px;padding:3px 9px;font-size:12px;cursor:pointer;font-family:'IBM Plex Mono',monospace;flex-shrink:0">⬇</button>
|
|
<button id="cup-btn-upload" onclick="cupDoUpload()" disabled style="background:color-mix(in srgb,#e89040 20%,#2a1e10);border:1px solid #e89040;color:#e89040;border-radius:5px;padding:3px 10px;font-size:12px;cursor:pointer;font-family:'IBM Plex Mono',monospace;font-weight:700;flex-shrink:0;white-space:nowrap">⚡ 업로드</button>
|
|
<button onclick="toggleUploadPanel()" style="background:none;border:none;color:#8a6840;cursor:pointer;font-size:16px;padding:0 6px;line-height:1;flex-shrink:0">×</button>
|
|
</div>
|
|
<!-- OTA 대상 입력줄 — 📡 OTA 버튼으로 접었다 폈다 -->
|
|
<div id="cup-ota-bar" style="display:none;align-items:center;gap:6px;padding:5px 8px;background:#241a0e;border-bottom:1px solid #5a3820;flex-shrink:0;flex-wrap:wrap">
|
|
<span style="font-size:11px;color:#c8a060;font-family:'IBM Plex Mono',monospace;flex-shrink:0">보드 IP</span>
|
|
<input id="cup-ota-ip" type="text" placeholder="192.168.0.42" style="width:130px;background:#1a1409;border:1px solid #5a3820;color:#e8c090;border-radius:4px;padding:3px 7px;font-size:12px;font-family:'IBM Plex Mono',monospace" />
|
|
<span style="font-size:11px;color:#c8a060;font-family:'IBM Plex Mono',monospace;flex-shrink:0">암호</span>
|
|
<input id="cup-ota-pw" type="password" placeholder="OTA_PASSWORD" style="width:130px;background:#1a1409;border:1px solid #5a3820;color:#e8c090;border-radius:4px;padding:3px 7px;font-size:12px;font-family:'IBM Plex Mono',monospace" />
|
|
<span style="font-size:11px;color:#8a7050;line-height:1.5">첫 업로드는 USB로 — OTA 코드가 보드에 들어가야 무선이 열립니다</span>
|
|
</div>
|
|
<progress id="cup-progress" value="0" max="100" style="display:none;width:100%;height:4px;border:none;border-radius:0;flex-shrink:0;accent-color:#e89040"></progress>
|
|
<!-- OUTPUT tab -->
|
|
<div id="cup-tab-log" style="flex:1;overflow-y:auto;padding:10px 14px;font-size:13px;line-height:1.8;white-space:pre-wrap;word-break:break-all;font-family:'IBM Plex Mono',monospace"></div>
|
|
<!-- 옵션 tab -->
|
|
<div id="cup-tab-opts" style="display:none;flex:1;overflow-y:auto;padding:12px 14px;font-size:13px;font-family:'IBM Plex Mono',monospace">
|
|
<div id="cup-info-rows" style="display:flex;flex-direction:column;gap:5px;margin-bottom:12px"></div>
|
|
<div id="cup-board-opts-list"></div>
|
|
<div id="cup-stm32-hint" style="display:none;margin-top:10px;padding:10px 12px;background:#2a1e10;border:1px solid #5a3820;border-radius:6px;color:#c8a060;font-size:13px;line-height:1.7">STM32 부트로더:<br>① BOOT0=3.3V ② RESET → ⚡ 업로드<br>업로드 후 BOOT0=GND → RESET</div>
|
|
<div id="cup-uf2-hint" style="display:none;margin-top:10px;padding:10px 12px;background:#2a1e10;border:1px solid #5a3820;border-radius:6px;color:#c8a060;font-size:13px;line-height:1.7">UF2 업로드:<br>① BOOTSEL 누른채 USB 연결<br>② BOOT 드라이브 나타남<br>③ ⬇ 다운로드 후 드라이브에 복사</div>
|
|
</div>
|
|
<!-- 시리얼 tab -->
|
|
<div id="cup-tab-mon" style="display:none;flex-direction:column;flex:1;min-height:0">
|
|
<div style="display:flex;align-items:center;padding:5px 10px;border-bottom:1px solid #5a3820;flex-shrink:0;gap:7px;flex-wrap:wrap;background:#241a0e">
|
|
<select id="cup-mon-baud" style="background:#3a2818;border:1px solid #5a3820;color:#c8a060;border-radius:4px;padding:3px 6px;font-size:12px;font-family:'IBM Plex Mono',monospace">
|
|
<option value="9600">9600</option><option value="19200">19200</option><option value="38400">38400</option>
|
|
<option value="57600">57600</option><option value="115200" selected>115200</option>
|
|
<option value="230400">230400</option><option value="921600">921600</option>
|
|
</select>
|
|
<select id="cup-mon-line-end" style="background:#3a2818;border:1px solid #5a3820;color:#c8a060;border-radius:4px;padding:3px 6px;font-size:12px;font-family:'IBM Plex Mono',monospace">
|
|
<option value="none">줄끝없음</option><option value="nl">LF</option><option value="cr">CR</option><option value="crnl" selected>CR+LF</option>
|
|
</select>
|
|
<button id="cup-mon-btn" onclick="cupMonToggle()" style="background:color-mix(in srgb,#22c55e 15%,#241a0e);border:1px solid color-mix(in srgb,#22c55e 35%,#241a0e);color:#4ade80;border-radius:4px;padding:3px 10px;font-size:12px;cursor:pointer;font-family:'IBM Plex Mono',monospace">▶ 연결</button>
|
|
<label style="display:flex;align-items:center;gap:4px;font-size:12px;color:#907050;cursor:pointer"><input type="checkbox" id="cup-mon-ts"> 시각</label>
|
|
<label style="display:flex;align-items:center;gap:4px;font-size:12px;color:#907050;cursor:pointer"><input type="checkbox" id="cup-mon-auto" checked> 자동스크롤</label>
|
|
<button onclick="cupMonClear()" style="background:none;border:none;color:#907050;cursor:pointer;font-size:12px;font-family:'IBM Plex Mono',monospace">지우기</button>
|
|
</div>
|
|
<div id="cup-mon-output" style="flex:1;overflow-y:auto;padding:8px 14px;font-size:13px;line-height:1.7;font-family:'IBM Plex Mono',monospace"></div>
|
|
<div style="display:flex;gap:7px;padding:7px 10px;border-top:1px solid #3a2818;background:#1e1408;flex-shrink:0">
|
|
<input type="text" id="cup-mon-input" placeholder="전송할 데이터..." disabled
|
|
onkeydown="if(event.key==='Enter')cupMonSend()"
|
|
style="flex:1;background:#2a1e10;border:1px solid #5a3820;color:#e8d0a0;border-radius:5px;padding:6px 10px;font-size:13px;font-family:'IBM Plex Mono',monospace;outline:none">
|
|
<button onclick="cupMonSend()" style="background:color-mix(in srgb,#c8a060 20%,#1e1408);border:1px solid #8a6840;color:#c8a060;border-radius:5px;padding:5px 14px;font-size:13px;cursor:pointer;font-family:'IBM Plex Mono',monospace">전송</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- 📱 USB 실기기 panel -->
|
|
<div id="android-usb-panel" style="display:none;flex-direction:column;flex-shrink:0;height:340px;min-height:120px;border-top:2px solid #1e5a8a;background:#0e1a24;position:relative">
|
|
<div id="android-usb-drag" style="position:absolute;top:0;left:0;right:0;height:5px;cursor:ns-resize;z-index:2;transition:background .15s"></div>
|
|
<div style="display:flex;align-items:center;height:32px;background:#122636;border-bottom:1px solid #1e5a8a;flex-shrink:0;padding:0 8px;gap:6px;overflow-x:auto">
|
|
<span id="android-usb-status" style="padding:2px 8px;border-radius:4px;font-size:11px;font-weight:700;background:#1a3448;color:#5aa8e8;flex-shrink:0;white-space:nowrap">대기 중</span>
|
|
<select id="android-usb-device-sel" style="display:none;background:#1a3448;border:1px solid #2a5a8a;color:#7ac0f0;border-radius:4px;padding:3px 6px;font-size:12px;font-family:'IBM Plex Mono',monospace;flex-shrink:0;max-width:180px"></select>
|
|
<input type="text" id="android-usb-pkg-filter" placeholder="패키지 필터 (선택, 예: com.example.app)"
|
|
style="flex:1;min-width:100px;background:#1a3448;border:1px solid #2a5a8a;color:#bcdcf8;border-radius:4px;padding:3px 8px;font-size:12px;font-family:'IBM Plex Mono',monospace;outline:none">
|
|
<button onclick="androidUsbRefreshDevices()" title="새로고침" style="background:#1a3448;border:1px solid #2a5a8a;color:#5aa8e8;border-radius:5px;padding:3px 10px;font-size:12px;cursor:pointer;font-family:'IBM Plex Mono',monospace;flex-shrink:0">🔄</button>
|
|
<button id="android-usb-install-btn" onclick="androidUsbInstallApk()" disabled style="background:#1a3448;border:1px solid #2a5a8a;color:#5aa8e8;border-radius:5px;padding:3px 10px;font-size:12px;cursor:pointer;font-family:'IBM Plex Mono',monospace;flex-shrink:0;white-space:nowrap">📦 APK 설치</button>
|
|
<button id="android-usb-logcat-btn" onclick="androidUsbOpenLogcat()" disabled style="background:#1a3448;border:1px solid #2a5a8a;color:#5aa8e8;border-radius:5px;padding:3px 10px;font-size:12px;cursor:pointer;font-family:'IBM Plex Mono',monospace;flex-shrink:0;white-space:nowrap">📜 Logcat</button>
|
|
<button id="android-usb-mirror-btn" onclick="androidUsbStartMirror()" disabled style="background:color-mix(in srgb,#5aa8e8 20%,#122636);border:1px solid #5aa8e8;color:#5aa8e8;border-radius:5px;padding:3px 10px;font-size:12px;cursor:pointer;font-family:'IBM Plex Mono',monospace;font-weight:700;flex-shrink:0;white-space:nowrap">🪞 화면 미러링</button>
|
|
<button onclick="toggleAndroidUsbPanel()" style="background:none;border:none;color:#3d6f9f;cursor:pointer;font-size:16px;padding:0 6px;line-height:1;flex-shrink:0">×</button>
|
|
</div>
|
|
<div id="android-usb-hint" style="display:none;padding:8px 14px;font-size:12px;color:#7ac0f0;background:#0f2233;border-bottom:1px solid #1e5a8a;line-height:1.6"></div>
|
|
<div id="android-usb-log" style="flex:1;overflow-y:auto;padding:10px 14px;font-size:13px;line-height:1.8;white-space:pre-wrap;word-break:break-all;font-family:'IBM Plex Mono',monospace;color:#a8ccec"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- Code folder browser dropdown (fixed, outside any overflow:hidden container) -->
|
|
<div id="code-folder-dropdown" style="display:none;position:fixed;z-index:9999;
|
|
background:var(--panel);border:1px solid rgba(34,197,94,0.4);border-radius:10px;
|
|
min-width:260px;max-height:320px;overflow-y:auto;
|
|
box-shadow:0 8px 32px rgba(0,0,0,0.35);padding:4px 0">
|
|
<div style="padding:6px 12px;font-size:10px;font-weight:700;color:rgba(34,197,94,0.9);
|
|
border-bottom:1px solid rgba(34,197,94,0.2);display:flex;justify-content:space-between;align-items:center">
|
|
<span id="code-dropdown-dir">📁 폴더 없음</span>
|
|
<span onclick="codeRefreshFolder()" style="cursor:pointer;opacity:0.7;font-size:13px" title="새로고침">↺</span>
|
|
</div>
|
|
<div id="code-folder-list" style="padding:2px 0">
|
|
<div style="padding:8px 12px;color:var(--muted);font-size:11px">로딩 중...</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="../js/code-editor/code.js?v=20260721b"></script>
|
|
<script>
|
|
function mobCloseAll(){
|
|
document.querySelector('.code-sidebar-wrap').classList.remove('mob-open');
|
|
document.getElementById('code-ai-panel').classList.remove('mob-open');
|
|
document.getElementById('mob-backdrop').classList.remove('show');
|
|
['files','ai'].forEach(t=>document.getElementById('mobtab-'+t)?.classList.remove('active'));
|
|
document.getElementById('mobtab-editor').classList.add('active');
|
|
}
|
|
function mobToggle(which){
|
|
if(window.innerWidth>1024)return;
|
|
const filesOpen=document.querySelector('.code-sidebar-wrap').classList.contains('mob-open');
|
|
const aiOpen=document.getElementById('code-ai-panel').classList.contains('mob-open');
|
|
if(which==='files'&&filesOpen){mobCloseAll();return;}
|
|
if(which==='ai'&&aiOpen){mobCloseAll();return;}
|
|
mobCloseAll();
|
|
if(which==='files'){
|
|
document.querySelector('.code-sidebar-wrap').classList.add('mob-open');
|
|
document.getElementById('mob-backdrop').classList.add('show');
|
|
document.getElementById('mobtab-files').classList.add('active');
|
|
document.getElementById('mobtab-editor').classList.remove('active');
|
|
} else {
|
|
document.getElementById('code-ai-panel').classList.add('mob-open');
|
|
document.getElementById('mob-backdrop').classList.add('show');
|
|
document.getElementById('mobtab-ai').classList.add('active');
|
|
document.getElementById('mobtab-editor').classList.remove('active');
|
|
}
|
|
}
|
|
</script>
|
|
<div id="csb-ctx"></div>
|
|
</body>
|
|
</html>
|