feat: 작업실 채팅 세션을 프로젝트별로 분리

기존엔 앱 전체 공용 세션(ws_main/랜덤 id) 하나를 써서 프로젝트를 바꿔도
옛 프로젝트의 대화가 그대로 남았다. sessionId를 'ws_'+projectId로 결정적
파생하도록 바꿔 프로젝트마다 독립된 대화를 갖고, 새로고침/재접속에도 같은
프로젝트의 대화를 이어받는다. ws_ 접두어는 유지(tool-scope 도구 개방 +
메인 사이드바 제외 근거). 프로젝트 전환/생성/삭제 시 채팅창을 해당
세션 기록으로 리셋(refreshChatForProject), 연속 전환 레이스는 시퀀스
가드로 처리. 앱세션 저장소(app-sessions.json) 의존 제거.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
kim
2026-09-22 19:39:10 +09:00
co-authored by Claude Code
parent 22cb075835
commit bff18650a5
+27 -13
View File
@@ -381,6 +381,7 @@ function switchProject(id){
clearStlSelection();
refreshAllPanes();
scheduleSave();
refreshChatForProject();
if(document.getElementById('pane-files').classList.contains('active'))loadFiles();
}
@@ -394,6 +395,7 @@ function createProjectPrompt(){
clearStlSelection();
refreshAllPanes();
scheduleSave();
refreshChatForProject();
if(document.getElementById('pane-files').classList.contains('active'))loadFiles();
}
@@ -408,6 +410,7 @@ function deleteProjectPrompt(){
clearStlSelection();
refreshAllPanes();
scheduleSave();
refreshChatForProject();
if(document.getElementById('pane-files').classList.contains('active'))loadFiles();
}
@@ -1050,26 +1053,26 @@ function onNotesChange(){
}
// ── 채팅 패널 ─────────────────────────────────────────────────────────────────
// 세션ID를 "ws_"로 시작하게 만들어서 tool-scope.ts가 이 세션에서는 키워드 없이도
// workshop_project 도구를 항상 열어주게 한다(doctor/lawyer 앱과 같은 패턴).
const APP_KEY='ws';
let _sessionId=null;
function getSessionId(){return _sessionId||'ws_main';}
async function initSessionId(){
try{const r=await fetch('/api/user/app-sessions',{headers:authH()});if(r.ok){const d=await r.json();if(d[APP_KEY]){_sessionId=d[APP_KEY];return;}}}catch{}
_sessionId='ws_'+Math.random().toString(36).slice(2,12);
try{await fetch('/api/user/app-sessions',{method:'PUT',headers:authH({'Content-Type':'application/json'}),body:JSON.stringify({appKey:APP_KEY,sessionId:_sessionId})});}catch{}
// 세션ID를 프로젝트별로 분리한다("ws_" 접두어는 유지 — tool-scope.ts가 이 세션에서는
// 키워드 없이도 workshop_project 도구를 항상 열어주고, 메인 사이드바 목록에서도 걸러진다).
// projectId 기반 결정적 파생이므로 앱세션 저장소(app-sessions.json) 없이도 새로고침/
// 재접속 시 같은 프로젝트의 이전 대화를 그대로 이어받는다. 옛 앱 전용 세션(ws_main/랜덤id)의
// 대화는 앱 전체 공용이던 터라 프로젝트 귀속이 불가 — 어느 프로젝트에도 더 안 보인다.
function getSessionId(){
const proj=currentProject();
return 'ws_'+(proj?proj.id:'noproject');
}
let _chatLoadSeq=0;
async function loadHistory(){
const sid=getSessionId();
if(!sid||sid.endsWith('_main'))return;
const seq=++_chatLoadSeq;
try{
const r=await fetch('/api/chat/sessions/'+encodeURIComponent(sid),{headers:authH()});
if(!r.ok)return;
const d=await r.json();
const msgs=(d.history||[]).filter(m=>m.role==='user'||m.role==='assistant');
if(!msgs.length)return;
if(!msgs.length||seq!==_chatLoadSeq)return;
for(const m of msgs.slice(-30)){
if(m.role==='user')addMsg('user',escHtml(m.content||''));
else addMsg('assistant',renderMd(m.content||''));
@@ -1119,6 +1122,18 @@ function renderMd(text){
return s;
}
function chatGreeting(){
return '🛠️ 작업실 채팅입니다. 현재 프로젝트: "'+(currentProject()?.name||'')+'". 부품 구매/상태 변경, 작업 완료 체크, 예산 확인, 새 프로젝트 생성 등을 물어보세요.';
}
// 프로젝트 전환(수동/생성/삭제) 시 채팅창을 그 프로젝트의 세션 기록으로 리셋한다 —
// 세션ID가 프로젝트별로 분리돼 있으므로 여기서 지우고 다시 당겨오는 것으로 충분하다.
async function refreshChatForProject(){
document.getElementById('chat-msgs').innerHTML='';
await loadHistory();
addMsg('system',chatGreeting());
}
async function sendMessage(){
const input=document.getElementById('chat-input');
const msg=input.value.trim();
@@ -1257,10 +1272,9 @@ document.addEventListener('click',(e)=>{
if(!(await checkAuth()))return;
{const tb=document.getElementById('theme-btn');if(tb)tb.textContent=document.documentElement.getAttribute('data-theme')==='light'?'☀️':'🌙';}
await loadWorkshopData();
await initSessionId();
await loadHistory();
loadModelPill();
addMsg('system','🛠️ 작업실 채팅입니다. 현재 프로젝트: "'+(currentProject()?.name||'')+'". 부품 구매/상태 변경, 작업 완료 체크, 예산 확인, 새 프로젝트 생성 등을 물어보세요.');
addMsg('system',chatGreeting());
document.getElementById('chat-input').focus();
})();
</script>