Files
homeclaw/web-ui/html/workshop-app.html
T
kimandClaude Sonnet 5 57cf65fd7e feat: 채팅·작업실 라이브 카메라 — 메시지마다 현재 화면 자동 첨부
- camera.js: getUserMedia 미리보기 패널, 전송 시 프레임(768px JPEG)을 /api/upload/image로 올려
  기존 이미지 첨부 경로(![📷…](/api/files/…) → resolveImageContent)로 모델에 전달. 음성 통화 STT도
  같은 전송 경로라 그대로 동작
- 메인 채팅: 📷 버튼+패널, sendChat에서 프레임 자동 첨부
- 작업실: 🎥 버튼+패널, 전송 훅·썸네일 표시·팝업에서 카메라 화면을 프로젝트 사진으로 저장
- handle-chat: 과거 턴의 카메라 프레임은 텍스트로 접어 비전 입력이 턴마다 누적되지 않게 함

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-26 20:37:16 +09:00

201 lines
11 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>
<link rel="icon" type="image/png" sizes="64x64" href="/homeclaw_logo.png">
<link rel="stylesheet" href="../css/styles.css">
<script>try{var _t=localStorage.getItem('homeclaw_theme')||'dark';document.documentElement.setAttribute('data-theme',_t);}catch(e){}</script>
<!-- go2rtc 라이브 플레이어(밀링 카드 ▶ 라이브용) — WebRTC→MSE→MJPEG 자동폴백 커스텀 엘리먼트 -->
<script type="module" src="/vendor/go2rtc/video-stream.js"></script>
<link rel="stylesheet" href="../css/workshop-app.css">
<link rel="stylesheet" href="/vendor/katex/katex.min.css">
<script src="/vendor/katex/katex.min.js"></script>
<script src="/vendor/katex/auto-render.min.js"></script>
<script src="/vendor/katex/mhchem.min.js"></script>
<script src="../js/app/katex-inline.js"></script>
</head>
<body>
<div class="rb-hdr">
<a href="/" style="text-decoration:none;font-size:13px;color:var(--muted)">← 홈</a>
<h1>🛠️ 작업실</h1>
<select class="ws-project-select" id="project-select" onchange="switchProject(this.value)"></select>
<button class="hdr-btn" onclick="createProjectPrompt()">+ 새 프로젝트</button>
<button class="hdr-btn" onclick="deleteProjectPrompt()">🗑 삭제</button>
<button class="hdr-btn" onclick="duplicateProjectPrompt()" title="구조를 그대로 복제(작업 완료·부품 상태는 초기화)">⧉ 복제</button>
<button class="hdr-btn" id="archive-btn" onclick="toggleArchiveProject()" title="목록에서 숨김(데이터·파일은 유지)">📦 보관</button>
<button class="hdr-btn" onclick="openHistory()" title="변경 이력 / 되돌리기 / 삭제된 프로젝트 복구">🕘 이력</button>
<label class="rb-hdr-check" title="보관된 프로젝트도 목록에 표시"><input type="checkbox" id="show-archived" onchange="toggleShowArchived(this.checked)">보관 포함</label>
<span class="rb-hdr-sp"></span>
<span class="rb-save-indicator" id="save-indicator"></span>
<button class="hdr-btn" id="theme-btn" onclick="toggleTheme()">🌙</button>
</div>
<div class="rb-conflict" id="conflict-banner"></div>
<div class="rb-budget">
<div class="rb-budget-item"><div class="rb-budget-label">검토중</div><div class="rb-budget-val" id="sum-review">0원</div></div>
<div class="rb-budget-item"><div class="rb-budget-label">주문완료</div><div class="rb-budget-val" id="sum-ordered">0원</div></div>
<div class="rb-budget-item"><div class="rb-budget-label">보유</div><div class="rb-budget-val" id="sum-owned">0원</div></div>
<div class="rb-budget-item total"><div class="rb-budget-label">총 예상비용</div><div class="rb-budget-val" id="sum-total">0원</div></div>
<div class="rb-budget-item" style="min-width:170px">
<div class="rb-budget-label">목표 예산</div>
<div class="rb-budget-target"><input type="number" min="0" step="10000" id="budget-input" placeholder="미설정" oninput="onBudgetChange()"><span style="font-size:12px">원</span></div>
<div class="rb-gauge" id="budget-gauge" style="display:none"><i id="budget-gauge-bar"></i></div>
<div class="rb-gauge-text" id="budget-gauge-text"></div>
</div>
</div>
<div class="rb-layout">
<div class="rb-main">
<div class="rb-tabs">
<button class="rb-tab-btn active" data-tab="parts" onclick="switchTab('parts')">부품</button>
<button class="rb-tab-btn" data-tab="tasks" onclick="switchTab('tasks')">작업</button>
<button class="rb-tab-btn" data-tab="assembly" onclick="switchTab('assembly')">조립</button>
<button class="rb-tab-btn" data-tab="equip" onclick="switchTab('equip')">장비</button>
<button class="rb-tab-btn" data-tab="files" onclick="switchTab('files')">파일</button>
<button class="rb-tab-btn" data-tab="overview" onclick="switchTab('overview')">개요</button>
<button class="rb-tab-btn" data-tab="notes" onclick="switchTab('notes')">메모</button>
</div>
<div class="rb-body">
<div class="rb-pane active" id="pane-parts">
<div class="rb-parts-bar">
<div class="rb-seg" id="parts-view-seg">
<button data-view="card" onclick="setPartsView('card')">카드</button>
<button data-view="table" onclick="setPartsView('table')">표</button>
</div>
<span id="parts-chips"></span>
<select id="parts-sort" onchange="setPartsSort(this.value)" title="정렬(화면 표시만 바뀌고 저장 순서는 유지)">
<option value="order">등록순</option>
<option value="name">이름순</option>
<option value="amount">금액 큰 순</option>
<option value="status">상태순</option>
</select>
<span class="rb-hdr-sp"></span>
<button class="rb-mini-btn" onclick="exportBom('csv')" title="엑셀에서 열 수 있는 CSV">⬇ CSV</button>
<button class="rb-mini-btn" onclick="exportBom('md')" title="마크다운 표">⬇ MD</button>
</div>
<div id="parts-container"></div>
<button class="rb-add-btn" onclick="addPart()">+ 부품 추가</button>
</div>
<div class="rb-pane" id="pane-tasks">
<div class="rb-prog-all" id="tasks-progress"></div>
<div id="phases-container"></div>
<button class="rb-phase-add" onclick="addPhase()">+ 단계 추가</button>
</div>
<div class="rb-pane" id="pane-assembly"></div>
<div class="rb-pane" id="pane-equip">
<div class="rb-equip-grid">
<div class="rb-equip-card">
<div class="rb-equip-hdr">
<span class="rb-equip-dot" id="k2-dot"></span>
<span>🖨️ K2 콤보</span>
<button class="rb-equip-refresh" onclick="refreshK2Widget()">↺</button>
</div>
<div class="rb-equip-frame-wrap">
<video id="k2-live" autoplay muted playsinline style="display:none;width:100%;height:100%;object-fit:contain"></video>
<img id="k2-frame" alt="" style="display:none">
<span class="rb-equip-placeholder" id="k2-placeholder">불러오는 중...</span>
<button class="rb-equip-live-btn" id="k2-live-btn" onclick="toggleK2Live()">▶ 라이브</button>
</div>
<div class="rb-equip-status" id="k2-status-text">-</div>
</div>
<div class="rb-equip-card">
<div class="rb-equip-hdr">
<span class="rb-equip-dot" id="mill-dot"></span>
<span>⚙️ CNC 밀링(프린터실)</span>
<button class="rb-equip-refresh" onclick="refreshMillWidget()">↺</button>
</div>
<div class="rb-equip-frame-wrap">
<video-stream id="mill-live" mode="webrtc,mse,mp4,mjpeg" style="display:none;width:100%;height:100%;background:#000;"></video-stream>
<img id="mill-frame" alt="" style="display:none" onerror="millFrameError()" onload="millFrameLoaded()">
<span class="rb-equip-placeholder" id="mill-placeholder">불러오는 중...</span>
<button class="rb-equip-live-btn" id="mill-live-btn" onclick="toggleMillLive()">▶ 라이브</button>
</div>
<div class="rb-equip-status">20초마다 자동 새로고침(스냅샷) — <a href="/html/milling-app.html" target="_blank" style="color:#f59e0b">밀링 앱에서 라이브로 보기 ↗</a><span id="mill-live-marker"></span></div>
</div>
</div>
</div>
<div class="rb-pane" id="pane-files">
<div class="rb-files-toolbar">
<select id="upload-folder-select"></select>
<button class="hdr-btn" onclick="promptNewFolder()">+ 새 폴더</button>
<label class="rb-upload-btn">📎 파일 업로드<input type="file" id="file-input" multiple style="display:none" onchange="handleFileInputChange(event)"></label>
<button class="hdr-btn" id="stl-slice-btn" onclick="openSlicerForSelected()" disabled>🔪 선택 STL 슬라이서로 (<span id="stl-sel-count">0</span>)</button>
</div>
<div class="rb-files-dropzone" id="files-dropzone">여기로 파일을 끌어다 놓거나 위 버튼으로 업로드 (사진/CAD/STL 등, 최대 100MB)</div>
<div id="files-list-container"></div>
</div>
<div class="rb-pane" id="pane-overview">
<textarea class="rb-notes" id="overview-textarea" placeholder="프로젝트 개요 — 무엇을 만드는지/목표/구성. 채팅에서 '개요 써줘'로도 쓸 수 있다." oninput="onOverviewChange()"></textarea>
</div>
<div class="rb-pane" id="pane-notes">
<textarea class="rb-notes" id="notes-textarea" placeholder="자유 메모..." oninput="onNotesChange()"></textarea>
</div>
</div>
</div>
<div class="rb-resize" id="resize-handle"></div>
<div class="rb-chat" id="chat-panel">
<div class="rb-chat-hdr">
<span class="rb-chat-label" id="chat-label">🛠️ 작업실 채팅</span>
<span class="rb-hdr-sp"></span>
<div class="rb-model-pill">
<button class="rb-model-pill-btn" id="model-pill-btn" onclick="toggleModelDropdown(event)" title="모델 변경">-</button>
<div class="rb-model-dropdown" id="model-dropdown"></div>
</div>
<button class="rb-chat-clear" onclick="clearChat()" title="대화 지우기">🗑️</button>
</div>
<div class="rb-msgs" id="chat-msgs"></div>
<!-- 라이브 카메라 패널: 켜 두면 메시지마다 현재 화면이 자동 첨부된다 (js/app/camera.js) -->
<div id="camera-panel" class="rb-cam-panel" style="display:none">
<video id="camera-video" autoplay muted playsinline></video>
<div class="rb-cam-ctl">
<label><input type="checkbox" id="camera-auto" checked> 메시지마다 현재 화면 자동 첨부</label>
<div class="rb-cam-btns">
<button class="rb-mini-btn" onclick="cameraSnapAttach()" title="지금 화면을 입력창에 첨부">📸 촬영</button>
<button class="rb-mini-btn" onclick="cameraFlip()" title="전면/후면 전환">🔄 전환</button>
<button class="rb-mini-btn" onclick="cameraClose()" title="카메라 끄기">✕</button>
</div>
</div>
</div>
<div class="rb-input-bar">
<button class="rb-attach-btn" id="camera-btn" onclick="toggleCamera()" title="카메라 켜기 (메시지마다 현재 화면 첨부)">🎥</button>
<div class="rb-attach-wrap">
<button class="rb-attach-btn" onclick="toggleAttachPop(event)" title="이 프로젝트의 사진을 채팅에 첨부(모델이 직접 봄)">📷</button>
<div class="rb-attach-pop" id="attach-pop"></div>
</div>
<textarea class="rb-textarea" id="chat-input" rows="1" placeholder="부품/작업/예산에 대해 물어보세요..." onkeydown="handleKey(event)" oninput="autoResize(this)"></textarea>
<button class="rb-send" id="send-btn" onclick="sendMessage()">전송</button>
</div>
</div>
</div>
<div class="rb-modal-bg" id="modal-bg" onclick="if(event.target===this)closeModal()">
<div class="rb-modal">
<div class="rb-modal-hdr"><span id="modal-title"></span><button onclick="closeModal()">✕</button></div>
<div class="rb-modal-body" id="modal-body"></div>
</div>
</div>
<script src="../js/app/workshop-core.js"></script>
<script src="../js/app/workshop-parts.js"></script>
<script src="../js/app/workshop-tasks.js"></script>
<script src="../js/app/workshop-files.js"></script>
<script src="../js/app/camera.js"></script>
<script src="../js/app/workshop-chat.js"></script>
<script src="../js/app/workshop-init.js"></script>
</body>
</html>