Add user workspaces, dental DB scripts, server memory + code editor improvements

- cherry: orofacial/TMD PPTX presentations + 음악가 구강건강 가이드
- papa: sinus/infraoccluded PPTX, garden-monitor Arduino, MIDI files
- databases: PCSP scraper, translation scripts, vision benchmark, batch JSONs
- web-ui: code.js/companion.py/styles.css major updates, autostart installer
- src: ollama-client, session, LLMProvider, ollama-adapter patches
- .gitignore: add claude-key, SQLite WAL/SHM, __pycache__, nohup.out

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kim
2026-06-07 23:13:03 +09:00
co-authored by Claude Sonnet 4.6
parent 43942d4b2c
commit 508d11615a
147 changed files with 10164 additions and 286 deletions
+204 -43
View File
@@ -31,6 +31,133 @@
--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-project-card {
background: var(--panel); border-color: var(--line);
}
:root:not([data-theme="dark"]) .csb-project-card:focus-within {
border-color: var(--brand); box-shadow: 0 0 0 2px rgba(22,104,227,.1);
}
:root:not([data-theme="dark"]) .csb-project-label { color: var(--brand); }
:root:not([data-theme="dark"]) .csb-project-select-wrap::after { color: var(--brand); }
:root:not([data-theme="dark"]) .csb-project-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); }
.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);}
@@ -70,9 +197,9 @@
<div class="code-sidebar-wrap">
<div id="sidebar-code" style="display:flex;flex-direction:column;flex:1;min-height:0;overflow:hidden">
<!-- 상단 탭: 파일 | 설정 -->
<div style="display:flex;border-bottom:1px solid var(--line);flex-shrink:0;background:var(--panel-2)">
<button class="code-sb-tab active" id="csb-tab-files" onclick="setCodeSbTab('files')">파일</button>
<button class="code-sb-tab" id="csb-tab-settings"onclick="setCodeSbTab('settings')">설정</button>
<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-settings"onclick="setCodeSbTab('settings')">⚙️ 설정</button>
</div>
<!-- 파일 탭: 상하 분할 -->
@@ -104,17 +231,43 @@
<div style="flex-shrink:0;height:46%;min-height:160px;display:flex;flex-direction:column;overflow:hidden">
<!-- 모델 선택 -->
<div style="display:flex;flex-direction:column;flex:1;overflow-y:auto;padding:10px 12px;gap:8px">
<div>
<div style="font-size:10px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:5px">Coder AI 모델</div>
<select id="csb-model-select" onchange="csbModelChanged()"
style="width:100%;font-size:12px;padding:5px 8px;background:var(--panel-2);color:var(--text);border:1px solid var(--line);border-radius:8px;cursor:pointer">
</select>
<div class="csb-model-card">
<div class="csb-model-label">
<span class="csb-model-dot"></span>
🤖 Coder AI 모델
</div>
<div class="csb-model-select-wrap">
<select id="csb-model-select" onchange="csbModelChanged()"></select>
</div>
</div>
<div id="csb-session-project-row" style="display:none">
<div style="font-size:10px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:5px">프로젝트</div>
<select id="csb-session-project-sel" onchange="switchProject(this.value||null)" style="width:100%;font-size:12px;padding:5px 8px;background:var(--panel-2);color:var(--text);border:1px solid var(--line);border-radius:8px;font-family:monospace;cursor:pointer"></select>
<div id="csb-session-project-row" class="csb-project-card" style="display:none">
<div class="csb-project-label">📁 프로젝트</div>
<div class="csb-project-select-wrap">
<select id="csb-session-project-sel" onchange="switchProject(this.value||null)"></select>
</div>
</div>
<button onclick="codeAiClear()" class="csb-new-session-btn">↺ 새 세션</button>
<!-- 실행 + 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>
</div>
<!-- 미리보기 -->
<div class="csb-term-row">
<button id="code-preview-toggle-btn" class="csb-term-btn" onclick="toggleCodePreview()" title="브라우저 미리보기 패널">🌐 미리보기</button>
</div>
<button onclick="codeAiClear()" style="width:100%;padding:7px;border:1px solid var(--line);background:var(--panel-2);color:var(--muted);border-radius:8px;cursor:pointer;font-size:12px">↺ 새 세션</button>
</div>
</div>
</div>
@@ -191,30 +344,18 @@
style="gap:4px;display:flex;align-items:center">
📁 <span>workspace/code/</span> ▾
</button>
<button class="code-tool-btn btn-ai" onclick="codeSendSelectionToAI()" title="선택 영역을 AI에게 전송">💬 AI에게 전송</button>
<div id="code-run-dropdown" style="position:relative;display:inline-flex;align-items:center">
<button class="code-tool-btn btn-run" onclick="codeRunDefault()" title="실행">
<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>
<span id="companion-dot" onclick="companionConnect()" style="width:8px;height:8px;border-radius:50%;background:#6b7280;display:inline-block;cursor:pointer;margin-left:6px;flex-shrink:0" title="Python companion 미연결 — 클릭해서 재연결"></span>
<div id="code-run-menu" style="display:none;position:absolute;top:100%;right: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-save" onclick="codeSaveFile()" title="파일 저장 (Ctrl+S)">💾 저장</button>
<button id="code-term-toggle-btn" class="code-tool-btn" onclick="toggleCodeTerminal()"
title="터미널 토글 (Ctrl+`)"
style="background:#111b14;border-color:#2d5a3a;color:#6fcf8a;font-family:'IBM Plex Mono',monospace;font-size:11px">⌨ 터미널</button>
</div>
</div>
<!-- Diff review bar (full-width, above editor) -->
<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>
<!-- Editor + AI panel -->
<div class="code-body">
<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 class="code-vresize" id="code-vresize"></div>
<div id="code-ai-panel">
@@ -231,27 +372,30 @@
<div class="code-ai-msg thinking">코드를 선택하고 "AI에게 전송"을 누르거나, 아래에 질문을 입력하세요.</div>
</div>
<!-- 세션 상태 바 -->
<div id="code-ai-session-bar" style="display:flex;align-items:center;padding:6px 10px;border-top:1px solid var(--line);background:var(--panel-2);flex-shrink:0;gap:8px">
<div id="code-ai-session-bar" style="display:flex;align-items:center;padding:5px 10px;border-top:1px solid #1a3a28;background:#0a1812;flex-shrink:0;gap:8px">
<!-- 세션 정보 -->
<div style="display:flex;align-items:center;gap:6px;min-width:0;flex:1;overflow:hidden">
<span style="font-size:12px;color:var(--muted);flex-shrink:0">세션</span>
<span id="code-ai-session-bar-project" style="font-size:12px;color:var(--text);font-family:monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:500">(전체)</span>
<span id="code-ai-session-bar-count" style="font-size:11px;color:var(--muted);flex-shrink:0"></span>
<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:12px;color:var(--text);white-space:nowrap;font-family:monospace"></span>
<div style="width:64px;height:6px;background:var(--line-strong);border-radius:4px;overflow:hidden;flex-shrink:0">
<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>
<!-- 새 세션 버튼 -->
<button onclick="codeAiClear()" title="새 세션 시작" style="flex-shrink:0;background:var(--panel-2);border:1px solid var(--line-strong);border-radius:6px;color:var(--text);cursor:pointer;padding:4px 10px;font-size:12px;line-height:1.5;white-space:nowrap;font-weight:500">↺ 새 세션</button>
</div>
<div class="code-ai-input-row">
<button id="code-ai-mode-btn" onclick="toggleCodeAiMode()" title="Ask: 질문/설명 | Code: 파일 편집"
style="flex-shrink:0;align-self:flex-end;background:none;border:1px solid #2a3a5a;border-radius:4px;padding:4px 8px;font-size:11px;cursor:pointer;color:#7aabf0;white-space:nowrap;line-height:1.4">✏️ Code</button>
<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();}"></textarea>
<button class="code-tool-btn primary" onclick="codeAiSend()" style="flex-shrink:0;align-self:flex-end">전송</button>
<button id="code-ai-send-btn" class="code-tool-btn primary" onclick="codeAiSend()" style="flex-shrink:0;align-self:flex-end">전송</button>
</div>
</div>
</div>
@@ -260,13 +404,30 @@
<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.py 필요)" style="background:none;border:none;color:#6b7280;cursor:pointer;font-size:12px;padding:0 6px;line-height:1;flex-shrink:0">🖥️</button>
<button onclick="addCodeTermShell()" title="새 터미널 (+)" style="background:none;border:none;color:#3d6b4f;cursor:pointer;font-size:16px;padding:0 8px;line-height:1;flex-shrink:0">+</button>
<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 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 onclick="toggleCodeTerminal()" title="닫기" style="background:none;border:none;color:#3d6b4f;cursor:pointer;font-size:14px;padding:0 8px;line-height:1;flex-shrink:0">&times;</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">&times;</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>
</div>
</div>