- ▶ 라이브 버튼 → 카드 안에서 상시 WebRTC 스트리밍(k2-app.html의 검증된 연결
로직 이식: STUN 필수·오퍼 null-candidate/1.5초 중 빠른 쪽 퀴크 포함)
- 라이브 중엔 20초 스냅샷 그랩(WebRTC 풀협상)을 건너뛴다 — 비디오가 이미
실시간이고 짧은 연결이 간섭하므로. 상태 조회(Moonraker)는 계속, 상태줄에
"● 실시간" 표시
- 장비 탭을 벗어나면 stopEquipTimers가 라이브 연결을 닫는다(안 보는 동안
자원 안 쓰기), ICE failed면 자동 종료 후 안내문
Co-Authored-By: Claude Code <noreply@anthropic.com>
저장 개편(리뷰 #4 근본치료):
- PUT /api/workshop/project/:id — 서버가 디스크의 현재 문서에 해당 프로젝트만
upsert. 대시보드 저장과 채팅 도구 저장이 겹쳐도 서로 다른 프로젝트 편집이
통째로 사라지지 않음. DELETE /api/workshop/project/:id,
PUT /api/workshop/active(포인터 단독) 신설. 옛 전체문서 PUT은 스테일 탭
호환으로 유지
- 클라이언트: dirtyProjects 추적 → saveProject는 바뀐 프로젝트만 순차 PUT,
활성 포인터는 putActive() 즉시 저장, 프로젝트 삭제는 DELETE 직접 호출,
beforeunload flush도 프로젝트 단위 keepalive PUT으로 전환
- upsertProject/removeProject를 순수함수로 추출, 단위테스트 4건
LOW 2건:
- loadFiles에 요청 일련번호 — 빠른 프로젝트 전환 시 옛 응답이 나중에 도착해
이전 프로젝트 파일을 새 판에 그리는 레이스 제거
- 슬라이서 인계 ?load=를 JSON 배열 인코딩으로 전환(파일명에 쉼표 있으면
옛 쉼표 join이 깨지던 문제). 슬라이서 쪽은 JSON 우선, 옛 형식 폴백 유지
Co-Authored-By: Claude Code <noreply@anthropic.com>
- 새 프로젝트 생성 직전 syncLocalToProject — debounce 미완료 편집 유실 방지
- softRefreshWorkshop: 활성 프로젝트가 서버쪽에서 바뀐 경우에만 clearStlSelection/
refreshChatForProject/loadFiles — 채팅에서 전환 시 옛 STL이 슬라이서로 오전송되던 문제
- beforeunload/pagehide에서 debounce 저장 flush(keepalive fetch) — 600ms 안에 탭 닫으면
마지막 편집이 사라지던 문제
- XSS: escJs에 "·<·> 이스케이프 추가(속성 경계 탈출 차단), chatGreeting 프로젝트명/
모델변경 실패 메시지 escHtml, 부품 참고링크 safeUrl로 javascript:/data: 스킴 차단
Co-Authored-By: Claude Code <noreply@anthropic.com>
- workshop-app: 메모 탭과 동일 구조의 "개요" 탭(textarea) 추가 — project.description
직접 편집, 채팅 set_description으로 쓴 내용은 응답 후 softRefresh로 갱신
- readme.md 자동생성(syncProjectReadmes) 철회 — 파일탭이 산만해져서 사용자 지정으로
탭 방식으로 전환, 생성된 개요 폴더들도 삭제. description/set_description은 유지
- server.ts IMAGE_TYPES: 텍스트 계열(.md/.txt/.csv 등) charset=utf-8 명시 — charset
없이 text/plain으로 뱉으면 브라우저가 latin1로 해석해 한글이 전부 깨짐(실측)
Co-Authored-By: Claude Code <noreply@anthropic.com>
기존엔 앱 전체 공용 세션(ws_main/랜덤 id) 하나를 써서 프로젝트를 바꿔도
옛 프로젝트의 대화가 그대로 남았다. sessionId를 'ws_'+projectId로 결정적
파생하도록 바꿔 프로젝트마다 독립된 대화를 갖고, 새로고침/재접속에도 같은
프로젝트의 대화를 이어받는다. ws_ 접두어는 유지(tool-scope 도구 개방 +
메인 사이드바 제외 근거). 프로젝트 전환/생성/삭제 시 채팅창을 해당
세션 기록으로 리셋(refreshChatForProject), 연속 전환 레이스는 시퀀스
가드로 처리. 앱세션 저장소(app-sessions.json) 의존 제거.
Co-Authored-By: Claude Code <noreply@anthropic.com>
- switchProject/createProject/deleteProject에서 stlSelection 초기화 누락 →
프로젝트 전환 후 슬라이서 전송 시 옛 프로젝트 relPath가 현재 프로젝트
경로로 재해석되어 같은 이름의 다른 파일이 슬라이싱될 수 있던 버그
- renderParts에서 status/qty/unitPrice 이스케이프 누락(채팅 도구가 임의
문자열 쓰면 HTML 주입 경로) → escAttr 적용
- 프로젝트 생성/삭제 시 파일 탭이 열려있으면 loadFiles 미호출로 옛
프로젝트 파일 목록 남던 버그 → switchProject와 동일 갱신 추가
- callChat 내 SSE 파싱 변수 parts가 전역 부품 배열 parts를 섀도잉 →
sseParts로 개명
Co-Authored-By: Claude Code <noreply@anthropic.com>
renderMd: 코드펜스 Placeholder 추출→연속 목록 ul 그룹화→단락 분할→블록태그 p
래핑 제외→Placeholder 복원 순으로 재작성. softRefreshWorkshop 추가(저장 대기
플러시 후 변경시에만 재렌더, expandedParts/notes 보존). escJs 추가해 삭제/토글
onclick 인젝션 방지. 테마 토글 아이콘(🌙/☀️) 동기화.
Co-Authored-By: Claude Code <noreply@anthropic.com>
하나의 요청("scad를 STL로 만들어줘")을 실제로 안정적으로 처리하기까지
여러 겹의 문제가 있었음 — 전부 실측(ws_ 세션 실제 호출)으로 확인하고 고침.
1. 진짜 원인: build-tools.ts에 stl_cad의 완전히 별개인 낡은 하드코딩
스키마 사본이 있어서, stl-cad.ts의 jsonSchema를 아무리 고쳐도 모델에게
전달 안 되고 있었음(registry.ts 등록과 별개로 이 파일도 갱신해야 함).
2. 액션 enum(stl_cad의 action="compile") 선택 자체가 경량모델에 어려운
것으로 3연속 실측 확인 — 전용 단일목적 도구 scad_to_stl 신설(scad_path/
output_path/part만 받음, 선택지 없앰). tool-scope.ts에 게이트 추가.
3. python_eval의 subprocess 차단 메시지에 대안 도구 힌트 추가 — 처음엔
워드바운더리(\b) 정규식이 pattern.source 문자열 자체와 안 맞는 버그로
한 번도 안 붙었던 것도 같이 고침.
4. hasStlCadKeyword가 짧은 재시도("다시 시도해봐")에서 원래 요청이 2턴
윈도우 밖으로 밀려나며 도구가 사라지는 문제 — 직전 어시스턴트 답변도
같이 보게 함(lastAssistantMsg를 tool-scope.ts까지 스레딩).
5. AUTO-RECOVER가 scad_to_stl의 정상 결과("200.0×110.0×277.5 mm", 방금
만든 파일 실측치)를 "검증 안 된 스펙 주장"으로 오판해 웹서치를 강제
재프롬프트하던 것 — retry-decisions.ts의 GROUNDING_TOOL_PATTERN에 추가.
이 패턴이 "검색결과 읽는 법" 안내 트리거와 공유돼있어서, CAD 결과에
그 안내가 붙어 모델이 엉뚱하게 답하는 부작용도 같이 분리해서 막음.
6. 워크숍 세션 컨텍스트(handle-chat.ts workshopProjectCtx) 확장: 활성
프로젝트 안 scad 파일이 하나뿐이면 되묻지 않고 자동 사용, STL
저장경로는 원본 scad 옆 print/ 서브폴더로 유도(CAD/print=인쇄용,
CAD/reference=참고용 관례 유지).
7. workshop-app.html: 채팅으로 새 파일이 생겨도 파일탭이 이미 열려있으면
자동 새로고침 안 되던 것 수정.
Co-Authored-By: Claude Code <noreply@anthropic.com>
DELETE /api/chat/sessions/:id가 세션 파일만 unlink하고 session.ts의
in-memory Map은 그대로 둬서, 같은 세션ID를 계속 재사용하는 앱(투자자·의사·
날씨·회계사·변호사·마음·작업실·탐정, 총 8개)에서 삭제 후 같은 세션에 새
메시지를 보내면 다음 saveSession() 때 메모리에 남아있던 옛 대화가 파일로
그대로 부활했음.
파일 삭제 후 clearHistory()(메모리+파일 둘 다 제대로 비우는 기존 함수)도
호출하도록 수정. 실제로 메시지→삭제→같은 세션 새 메시지 시나리오로 재현
검증(부활 안 됨 확인).
워크숍 앱 채팅 헤더 CSS도 방어적으로 수정 — 삭제 버튼에 flex-shrink:0이
없어서 좁은 폭에서 라벨/모델pill에 밀려 안 보일 수 있던 것 고침.
Co-Authored-By: Claude Code <noreply@anthropic.com>
- slicer-view.js: ?load=<경로1,경로2,...> URL 파라미터 지원 추가 —
cad-editor-view.js의 ?path= 패턴과 동일한 취지로, 페이지 진입 시
지정한 워크스페이스 STL들을 /api/cad/file로 받아 자동으로
슬라이서 판에 배치한다
- workshop-app.html: 파일 탭 STL 카드에 다중선택 체크박스 추가 +
"선택 STL 슬라이서로" 툴바 버튼 — 체크한 부품 STL들을 K2 슬라이서
팝업(slicer-app.html)에 한 번에 올려 같은 판에서 배치/슬라이싱
기존 slicer-app.html/PrusaSlicer 파이프라인은 그대로 재사용, 새 백엔드
라우트 없이 routes-cad.ts의 /api/cad/file만 재사용.
Co-Authored-By: Claude Code <noreply@anthropic.com>
- routes-workshop.ts: 파일 목록/업로드/폴더생성/삭제 API 추가
(case-storage.ts 재사용, appType='workshop', caseId=projectId)
- workshop-app.html: 파일 탭 추가 — 드래그앤드롭 업로드, 폴더 그룹핑,
사진 썸네일, STL 클릭 시 Three.js CAD 편집기 팝업으로 연동
- cad-editor-view.js: ?path= 쿼리로 특정 STL 자동 로드 지원
- server.ts: registerWorkshopRoutes에 isPathInsideDir 전달
- stl-cad-core.ts: 평면뷰 렌더 카메라가 원점이 아닌 바운딩박스
중심을 look-at하도록 수정 — 원점에서 떨어진 모델 위쪽 잘림 해결
Co-Authored-By: Claude Code <noreply@anthropic.com>
메인 채팅의 전체 설정 모달을 그대로 들여오는 대신, 같은 백엔드
엔드포인트(/api/settings/model, /api/settings/provider, /api/models/test)만
재사용해 채팅 헤더에 작은 pill+드롭다운을 추가. 클릭하면 ollama provider가
보유한 로컬+클라우드 모델 태그 목록을 보여주고 선택 시 전역 models.primary를
바꾼다. curl로 GET/POST 전 구간 실측 확인(목록 12개 모델 조회, 전환→원복).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
단순 6파라미터 모달을 PrusaSlicer --load <ini> 기반 풀 에디터로 교체.
베이스 ini(K2_BASE_INI, 약 60키 큐레이션)를 TS 문자열로 임베드하고
buildProfileIni가 오버라이드 머지 + canonical start_gcode(13줄 K2 프라이밍
시퀀스, placeholder 온도 치환)를 강제 주입 — k2_config.ini 원본의 3줄
start_gcode를 그대로 쓰면 프라이밍이 날아가 출력이 망가지는 회귀 회피.
웹 UI: 9개 섹션(기본/소재·온도/서포트/브림/속도/팬/표면/리트랙션/고급)
약 40파라미터 편집 + 프로파일 저장/불러오기/삭제. 속도·소재 프리셋 연동.
라우트: /api/k2/profile/base·/profiles CRUD + /api/k2/slice?profile= 변경.
print3d_model 채팅도구는 기존 buildBaseArgs 경로 유지(동작 변경 회피).
Co-Authored-By: Claude Code <noreply@anthropic.com>
채팅으로 좌표를 말로 불러주는 게 너무 번거롭다는 피드백으로, stl_cad
채팅도구와 같은 기능을 드롭다운+버튼 UI로도 쓸 수 있게 함.
- stl-cad-core.ts: stl-cad.ts에서 OpenSCAD 실행/STL 파싱/렌더링 로직을
추출한 공용 모듈 + 워크스페이스 내 STL 파일 재귀 검색(findStlFiles)
추가. stl-cad.ts(채팅도구)와 routes-cad.ts가 공유
- routes-cad.ts: GET /api/cad/list(STL 목록), /api/cad/info(치수),
/api/cad/render(PNG 스트리밍), POST /api/cad/add-hole
- k2-app.html: "🔧 STL 확인" 모달 — 파일 드롭다운 선택 → 7방향 뷰 버튼 →
실시간 렌더 미리보기+치수, 접이식 "구멍 뚫기" 폼(x/y/z/지름/길이/축).
브라우저로 실제 렌더링/각도전환 확인함
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
로봇 프로젝트 하나만 다루던 대시보드/도구를 로봇 외 다른 메이커 프로젝트
(3D프린팅, 밀링 등)도 같은 틀로 관리할 수 있게 재설계. 사용자가 이름을
"작업실"로 정함.
- workshop-storage.ts(구 robot-storage.ts): 단일 문서 대신
{projects:[], activeProjectId} 구조. 기존 실사용 중이던
robot-project.json이 있으면 빈 시드로 덮어쓰지 않고 그 실제 데이터를
그대로 첫 프로젝트("로봇 프로젝트")로 마이그레이션
- routes-workshop.ts(구 routes-robot.ts): GET/PUT /api/workshop/data
- workshop-project.ts(구 robot-project.ts) 채팅도구: project_name 파라미터
추가(생략시 활성 프로젝트 대상), list_projects/create_project/
set_active_project 액션 신설
- workshop-app.html(구 robot-app.html): 헤더에 프로젝트 선택 드롭다운 +
새 프로젝트/삭제 버튼 추가. 장비(K2/밀링) 탭은 프로젝트와 무관하게
전역 유지
- 세션 접두사 rb_→ws_(tool-scope.ts의 isWorkshopAppSession), 도구명
robot_project→workshop_project
- 마이그레이션+새 프로젝트 생성/전환/삭제+채팅 연동까지 브라우저로 실제
확인함
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
각 부품 카드에 라벨+URL 쌍을 여러 개 붙일 수 있는 "정보" 섹션 추가 —
파는 곳, 대체품, 업그레이드 옵션, 관련 GitHub 레포 등 무엇이든 자유롭게
기록. URL을 입력하면 옆에 ↗ 열기 버튼이 자동으로 뜬다(타이핑 중 포커스가
끊기지 않게 그 버튼만 DOM에서 직접 갱신, 전체 리렌더 안 함).
- robot-storage.ts: RobotPart에 links: RobotPartLink[] 필드 추가(기존
저장된 데이터는 링크 없이 시작, 별도 마이그레이션 불필요)
- robot-project.ts: add_part 기본값에 links:[] 추가, get 응답에 부품별
링크 목록도 같이 표시
- robot-app.html: 카드 펼침 영역에 라벨/URL 입력 행 + 추가/삭제 버튼
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
K2 콤보 CFS 시스템은 출력 시작할 때 터치스크린에서 "필라멘트 삽입"을 직접
눌러 확인해야 실제 급지가 되는데, API로 원격 시작하면 아무도 그 확인을
못 눌러줘서 압출이 안 되고 Klipper 안전장치로 히터가 자동으로 꺼지는 걸
오늘 실제로 겪음(2026-09-20). 웹 UI 슬라이싱 모달과 print3d_model 채팅도구
둘 다 출력 시작 성공 메시지에 "터치스크린 확인하라"는 안내를 추가.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
로컬 PC의 STL 파일을 채팅 없이 K2 앱에서 바로 업로드→슬라이싱→K2 업로드까지
할 수 있는 모달 추가. print3d_model 채팅도구와 로직을 공유하도록
k2-slicer-core.ts로 공통 부분(프린터 프로파일, PrusaSlicer 실행, Moonraker
업로드, 시간 파싱)을 분리했다.
- k2-slicer-core.ts: print3d.ts에서 추출한 공용 슬라이싱 로직 + 오늘 실측한
FAST_SPEED_ARGS(퍼리미터/인필/트래블 속도업, 브릿지는 유지)
- routes-k2.ts: POST /api/k2/slice(STL 원본 바이트 바디+쿼리스트링 설정,
--duplicate로 동일 파츠 복제 지원 — 서로 다른 파츠 합치기(--merge)는 이
헤드리스 환경에서 세그폴트 나서 미지원), POST /api/k2/print-start(Moonraker
프록시, mixed-content 회피)
- k2-app.html: 헤더에 "🔪 슬라이싱" 버튼 → 모달(파일업로드/수량/소재/레이어
높이/인필/속도/서포트) → 결과+출력시작 버튼. 실제 파일 업로드로
슬라이싱→K2업로드까지 브라우저에서 확인함(출력 시작은 실제 프린트 중이라
테스트 안 함)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
행마다 항상 다 펼쳐져 있던 표 대신, 이름/상태뱃지/소계만 보이는 헤더를
클릭하면 수량·단가·상태·메모 편집 필드가 펼쳐지는 카드로 교체. 펼침
상태는 화면 전용(서버 저장 안 함)이라 다시 그려도 유지됨.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
부품 리스트를 보면서 바로 옆에서 로봇 프로젝트 얘기를 할 수 있게 리사이즈
가능한 채팅 패널을 대시보드에 붙임. 세션ID를 "rb_"로 시작하게 해서
tool-scope.ts가 이 세션 안에서는 키워드 매칭 없이 robot_project 도구를
항상 열어주도록 예외 처리(doctor_app의 'dr_'/lawyer_app의 'lw_'와 동일한
패턴) — "이 부품 지워줘"처럼 짧게 말해도 바로 동작한다.
채팅 응답이 끝나면 대시보드(부품표/예산/작업)를 자동으로 다시 불러와서
채팅으로 바꾼 내용이 즉시 화면에 반영되게 함.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
로봇 프로젝트 대시보드에 실제 공작 장비(K2 3D프린터, CNC 밀링 프린터실
PTZ카메라) 상태를 보는 "장비" 탭을 추가. 우선 가벼운 주기적 스냅샷 방식으로
구현(실시간 WebRTC 상시연결은 자원 부담 커서 보류, 추후 업그레이드 검토).
- routes-k2.ts: GET /api/k2/status — Moonraker print_stats/virtual_sdcard를
요약해서 반환(온라인여부/상태/파일명/진행률/경과시간)
- nvr.ts: GET /api/nvr/frame/:channelId — go2rtc(127.0.0.1:1985)의
frame.jpeg를 프록시(밀링 PTZ카메라, channelId=1000 "프린터실"), go2rtc
포트를 브라우저에 직접 노출하지 않음
- robot-app.html: "장비" 탭 — K2는 20초 간격으로 짧게 WebRTC 연결→ontrack에서
캔버스 캡처→즉시 종료(상시 연결 안 함), 밀링은 위 프록시로 img 새로고침.
"장비" 탭을 보고 있을 때만 폴링, 다른 탭으로 나가면 타이머 정지
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
메카넘휠 이동로봇 프로젝트(파이5+RPLidar, 추후 팔 추가)의 부품리스트/
단계별 작업/예산을 관리하는 웹앱. doctor-app의 GET/PUT 단일문서 패턴을
재사용하되 케이스 목록 없이 단일 프로젝트 문서로 단순화했고, 첫 로드시
project_robot_dog.md 메모 기준 실제 진행상황을 기본 시드로 반환한다.
- src/gateway/routes/routes-robot.ts: GET/PUT /api/robot/project,
사용자별 workspace/.smallclaw/robot-project.json에 원자적 저장
- web-ui/html/robot-app.html: 부품/작업/메모 3탭, 실시간 예산 요약,
디바운스 자동저장
- index.html 🏠 홈 드롭다운에 🤖 로봇 링크 추가
장비연동/전용챗봇/외부데이터연동/CAD플러그인은 다음 단계로 보류.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
onicecandidate가 ev.candidate===null(전체 gathering 종료)까지 기다린 뒤에만 오퍼를
보내고 있었는데, 이 네트워크에서 그 신호가 20~30초씩 걸림(가상 인터페이스들 순회+
타임아웃으로 추정). 실제 필요한 STUN srflx 후보는 1~2초면 잡히므로, 완전종료를
기다리지 않고 1.5초 타임아웃 뒤 그때까지 모인 후보로 바로 오퍼를 보내도록 변경.
브라우저 실측: 연결까지 20~30초 → 3초 이내로 단축.
- nvr-app.html: go2rtc WS URL을 페이지 오리진 기준 상대경로로 (하드코딩된 ws://LAN IP가
HTTPS 페이지에서 mixed-content로 막히던 문제)
- k2-app.html: 카메라 재연결 시 오래된 연결의 실패 콜백이 새 연결 상태를 덮어쓰던 레이스 수정
- routes-k2.ts: K2 시그널링 fetch에 타임아웃 추가(프린터 응답없음 시 무한대기 방지)
- nvr.ts/routes-comfyui.ts/routes-android.ts에 중복돼있던 raw WebSocket 업그레이드
프록시(connect→pipe→cleanup)를 ws-proxy-util.ts의 공통 헬퍼로 통합
Xiongmai NVR이 3rd파티 카메라(Mercusys)를 "Resolution too big" +
online=false로 계속 보고하는데, 실제로는 RTSP 패스스루가 정상 동작함
(/ch5_0.264 프레임 정상 수신 확인). 그런데 quadChannels 필터가 c.online만
보고 있어서 마당이 6분할에서 빈 슬롯으로 렌더됐음.
chnStreamable() = online || "resolution too big" 상태 → 이런 채널도
6분할·라이브 버튼에 포함. 진짜 죽은 채널은 img.onerror가 "영상 없음" 처리.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017yEadXvr9ZWp2S5iRB1B25
검토만 하던 작가 앱에 실제 원고 문장을 써주는 기능 추가.
- 원고 뷰 하단에 프롬프트 입력창 + [이어쓰기][새 장면][선택부분 다시쓰기]
- POST /api/writer/compose: 장르·요소·설정노트를 프롬프트에 실어 프로즈 생성
(temp 0.85로 검토보다 창작적, "알겠습니다" 머리말 없이 본문만)
- 생성물은 "AI 초안" 미리보기 → [원고에 넣기]/[다시]/[버리기]. 이어쓰기·새 장면은
원고 끝에 append, 다시쓰기는 선택 범위를 치환
- 요소가 자동 반영됨을 알리는 힌트("반영 중인 요소: 주제, 문체, 배경 …")
브라우저에서 새 장면·이어쓰기·삽입까지 재현·검증 완료.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBDYbeeazVwgQ7RruCXRgY
기존: 요소(주제/문체/구성/배경/인물) = 누르면 바로 AI 비평이 도는 전문가 7명.
요소를 입력할 자리가 없어 "요소 다 채우고 검토받기"라는 자연스러운 흐름이 불가능.
- 요소 = 작품에 저장되는 입력칸 5개 (works.json에 elements 객체, 옛 작품은 읽을 때 정규화)
- 원고 ↔ 요소 뷰 토글, 사이드바 요소 버튼은 해당 입력칸으로 이동
- "📋 전체 검토": expertType='review' — 총괄 편집자가 요소별 의도 vs 원고 실행의
간극·강점·우선순위를 종합 분석
- 개별 "💬 이 요소 검토": 그 요소의 작가 의도를 프롬프트에 주입해 의도-실행 대조
- saveWork/onWorkSelect가 elements를 함께 저장·복원
브라우저에서 전체 흐름 재현·검증 완료.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBDYbeeazVwgQ7RruCXRgY
주제→문체 등으로 빠르게 전환하면 selectExpert가 sendChat(true)를 부르는데
chatBusy 가드에 걸려 새 전문가 요청은 안 나가고, 먼저 나간 요청 결과가
돌아올 때 activeExpert가 바뀐 걸 확인 안 해서 새 전문가 대화창에 그대로
push+render 됐다("모델이 가로챔").
AbortController + 세대 카운터(chatGen)로 수정: 전문가 전환·새 메시지마다
진행 중 요청을 abort하고 세대를 올리며, 응답이 돌아오면 자기 세대가
아직 최신인지 확인 후에만 반영. 늦게 끝난 구세대 요청은 UI를 안 건드림.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBDYbeeazVwgQ7RruCXRgY
- 판례 탭을 맨 왼쪽으로 옮기고 기본 검색 대상으로 바꿨다. 법률앱에서 가장
자주 쓰는 게 판례다.
- 결과 목록 위에 좁히기 입력칸. 사건명·법원·사건번호에서 찾는다. 서버를 다시
부르지 않고 이미 받아온 결과를 숨김/표시로 거르므로 즉시 반응하고 law.go.kr
쿼터도 안 쓴다. 새 검색이 오면 조건을 비운다(안 그러면 결과가 있는데 빈
화면으로 보인다). 1건이면 좁힐 게 없으므로 입력칸을 숨긴다.
- 결정례 4종(헌재·법령해석·행정심판·심판)은 드롭다운으로 묶었다. 탭이 8개가
되면 좁은 화면에서 줄바꿈이 생긴다. 드롭다운 항목을 고르면 탭 하이라이트를
끄고 드롭다운이 현재 선택을 나타낸다.
- 키가 없어 검색 자체가 불가능한 경우(미국 판례)를 "결과 없음"과 구분해
안내를 띄운다. 예전엔 "다른 키워드로 검색해보세요"가 떠서 헛되이 재시도했다.
부트스트랩에서 setSearchType()을 부른다. 이 함수는 탭 클릭 때만 돌아서 최초
로드에는 안 걸렸고, 그래서 기본값이 판례인데 placeholder는 법령용이었다 —
📁 버튼이 안 보이던 것과 같은 종류의 누락이라 이번엔 같이 잡았다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
상담하듯 일반 대화로 물어보다가 사건으로 승격하고 싶을 때 쓴다. 채팅 헤더의
📁 버튼(사건 미선택일 때만 노출).
요약만 옮기면 사건 안에서 이어 물을 때 맥락이 끊기므로 **대화 세션 파일 자체를**
사건 세션(`lw_case_<id>`)으로 옮긴다. 그래서 서버에 세션 이동 API를 새로 만들었다
(POST /api/chat/sessions/:id/move). 파일명으로 그대로 쓰이므로 세션 ID는
[A-Za-z0-9_-]만 허용하고, 목적지가 이미 있으면 409로 거부한다 — 덮어쓰면 다른
사건의 대화가 날아간다.
실패 처리는 되돌릴 수 없는 쪽을 기준으로 잡았다:
- 제목/개요 추출이 실패해도 진행한다. 첫 질문을 제목으로 쓴다. 이름은 나중에
고치면 되지만 대화는 못 되찾는다.
- 세션 이동이 실패하면 사건을 만들지 않는다. 사건만 생기고 대화가 안 따라오면
일반창에 그대로 남아 중복이 된다.
버튼 표시 토글을 updateToCaseBtn()으로 빼서 부트스트랩에서도 부른다.
처음엔 ensureChatContextForCase 안에만 넣었는데, 그 함수는 모드 전환·사건
선택·대화 지우기에서만 돌아서 최초 로드에는 안 걸렸다 — 버튼이 계속 숨어 있었다.
가드 검증: 없는 세션 404 / 경로조작 from·to 400 / 인증 없음 401.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
법률앱에서 `$\rightarrow$`가 날것으로 보인다는 신고. 통화 `$` 문제(f06955f)를
먼저 의심했지만 원인은 더 단순했다 — **앱 페이지들은 KaTeX를 아예 로드하지
않고 있었다.**
index.html katex O
lawyer / detective / doctor / investor / accountant / mind / weather 전부 X
메인 채팅에만 chat-render.js의 renderKatexIn이 있었고, 앱 7개는 각자 자기
renderMd를 갖고 있어 수식이 통과할 길이 없었다. 공용 js/app/katex-inline.js로
빼서 전부에 붙였다(통화 `$` 보호 로직 포함).
addMsg에서 DOM 삽입 직후 호출한다. innerHTML은 <script>를 실행하지 않으므로
삽입 시점마다 명시적으로 불러야 한다. TTS 버튼이 innerText를 읽어가므로
그보다 먼저 렌더링되게 배치했다 — 안 그러면 음성이 "달러 백슬래시 rightarrow
달러"를 읽는다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"어휘카드를 추가해도 자꾸 없어진다" 리포트. 카드는 custom-cards.json으로
이미 서버에 있었는데 **카테고리는 localStorage에만** 있었다. 브라우저
데이터를 지우거나 다른 기기에서 열면 카테고리 버튼이 통째로 사라지고,
그 카테고리로 만든 카드는 분류 탭에서 안 보인다(전체 탭에만 남아
"없어진" 것처럼 보인다).
custom-categories.json에 언어별로 저장한다. 병합 방식인 게 중요하다 —
클라이언트가 보낸 목록으로 덮어쓰면 localStorage가 빈 새 기기에서 앱을
한 번 여는 것만으로 서버 카테고리가 전부 날아간다. 빈 배열을 보내도
기존 값이 유지되는 걸 확인했다. 로컬에만 있던 카테고리는 첫 접속 때
서버로 올라가므로 지금 브라우저에 남아 있는 건 손실 없이 넘어간다.
같이 고친 것: saveCustomCards가 .catch(()=>{})로 실패를 삼키면서 호출부는
"✅ N개 추가되었습니다"를 띄우고 있었다. 세션 만료로 POST가 401이 나도
사용자는 성공한 줄 알고, 새로고침하면 카드가 없다 — 리포트된 증상과
정확히 같은 모양이다. 이제 저장을 기다린 뒤 결과에 맞는 문구를 띄운다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
사용자 지적: "구름이 아니고 파일 버튼인데?"
같은 줄에 이미 💾(파일 저장)와 📁(새 폴더)가 있는데 로컬 모드를 💾로 표시해서,
바로 옆 저장 버튼과 똑같아 보였다. ☁️/💾 한 쌍만 보고 골랐지 그 줄에 뭐가 이미
있는지 안 봤다.
🖴 같은 다른 이모지로 바꾸는 대신 "로컬"/"서버" 글자로 현재 위치를 그대로 적는다.
아이콘 추측이 필요 없고 글꼴에 따라 안 보일 걱정도 없다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
에디터는 File System Access API로 브라우저가 고른 로컬 폴더만 읽고 썼다. 그래서
서버에서 파일을 고쳐도 에디터에는 닿지 않는다 — 2026-08-15에 실제로 서버 사본을
고쳐놓고 보드에는 에디터의 다른 파일이 올라가, 같은 스케치가 두 벌로 갈린 채
한참을 헤맸다.
서버 API(/api/code/files, /api/code/file, /api/code/save)는 이미 다 구현돼 있었는데
프런트가 한 번도 부르지 않고 있었다. 코드에 "In server mode with activeProjectName"
같은 주석만 남아 있는 걸로 보아 예전에 만들려다 만 흔적이다.
호출부가 15곳이라 그쪽을 다 고치는 대신 localReadFile/localWriteFile/localListFiles/
localDeleteFile 네 개 원시함수만 서버로 분기시켰다. 로컬 폴더 유무로 막던 가드
9곳은 codeStorageReady()로 바꿔 서버 모드에서도 통과한다.
업로드는 그대로 로컬이다. Web Serial은 브라우저에서만 가능하지만, 굽는 대상이
에디터 버퍼(_cupCode)라 파일이 어디서 왔든 상관없다 — 저장 위치만 옮겨간다.
로컬 PTY 셸(1822행)과 Bash 도구(3140행)는 성격상 로컬 전용이라 제외했다.
함께 고친 백엔드 결함: POST /api/code/save가 path.basename()으로 경로를 납작하게
만들어 `projA/hello.ino`와 `projB/hello.ino`가 같은 `hello.ino`로 떨어졌다. 읽기는
이미 하위 경로를 지원하고 있어서 읽은 곳과 쓴 곳이 어긋나는 상태였다. resolve +
접두사 검사로 탈출만 막고 하위 경로는 허용하도록 수정.
실측: projA/projB에 같은 파일명으로 저장 → 서로 덮어쓰지 않음, 목록에 경로 그대로
노출, `../../../tmp/pwned.txt`는 403.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
인버터 옆처럼 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>