feat: 작업실 앱 파일 탭에 K2 슬라이서 연동
- 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>
This commit is contained in:
@@ -184,6 +184,11 @@ body{background:var(--bg);color:var(--text);font-family:system-ui,sans-serif;dis
|
||||
.rb-file-card:hover .rb-file-del{opacity:1;}
|
||||
.rb-file-del:hover{background:#ef4444;}
|
||||
.rb-file-card a{display:block;color:inherit;text-decoration:none;}
|
||||
/* STL 카드 전용 — 슬라이서로 보낼 파일 다중선택 체크박스(좌상단). 평소엔 델버튼처럼
|
||||
숨어있다 호버 시 보이되, 체크된 상태면 계속 보임(선택 중임을 표시). */
|
||||
.rb-file-stl-sel{position:absolute;top:4px;left:4px;z-index:2;width:16px;height:16px;cursor:pointer;opacity:0;transition:.15s;accent-color:#f59e0b;}
|
||||
.rb-file-card:hover .rb-file-stl-sel,.rb-file-stl-sel:checked{opacity:1;}
|
||||
.rb-file-card.stl-selected{outline:2px solid #f59e0b;outline-offset:-1px;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -255,6 +260,7 @@ body{background:var(--bg);color:var(--text);font-family:system-ui,sans-serif;dis
|
||||
<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>
|
||||
@@ -777,6 +783,7 @@ function deleteTask(phaseId,taskId){
|
||||
// 목록을 받아 그때그때 다시 그린다.
|
||||
const FILE_ICONS={photo:'🖼️',video:'🎬',document:'📄',other:'📦'};
|
||||
let filesState={files:[],folders:[]};
|
||||
let stlSelection=new Set(); // 슬라이서로 보낼 STL relPath 다중선택(09-22, K2 슬라이서 연동)
|
||||
|
||||
async function loadFiles(){
|
||||
const proj=currentProject();
|
||||
@@ -792,6 +799,9 @@ async function loadFiles(){
|
||||
container.innerHTML='<div class="rb-files-empty">파일 목록을 불러오지 못했습니다.</div>';
|
||||
return;
|
||||
}
|
||||
// 삭제된 파일이 남아있지 않게 선택 상태 정리.
|
||||
const liveRelPaths=new Set(filesState.files.map(f=>f.relPath));
|
||||
for(const rp of [...stlSelection])if(!liveRelPaths.has(rp))stlSelection.delete(rp);
|
||||
renderFiles();
|
||||
}
|
||||
|
||||
@@ -836,6 +846,10 @@ function renderFiles(){
|
||||
${grid}
|
||||
</div>`;
|
||||
}).join('');
|
||||
const btn=document.getElementById('stl-slice-btn');
|
||||
const countEl=document.getElementById('stl-sel-count');
|
||||
if(countEl)countEl.textContent=String(stlSelection.size);
|
||||
if(btn)btn.disabled=stlSelection.size===0;
|
||||
}
|
||||
|
||||
const STL_RE=/\.stl$/i;
|
||||
@@ -847,7 +861,11 @@ function renderFileCard(file){
|
||||
: `<span class="rb-file-icon">${icon}</span>`;
|
||||
// STL은 새탭 다운로드 대신 Three.js CAD 편집기 팝업으로 연다(작업실↔K2 CAD 플러그인 연동).
|
||||
const openAttrs=isStl?`href="javascript:void(0)" onclick="openCadEditorForFile('${escAttr(file.relPath)}')"`:`href="${escAttr(file.url)}" target="_blank" rel="noopener"`;
|
||||
return `<div class="rb-file-card" data-rel="${escAttr(file.relPath)}">
|
||||
// STL만 슬라이서로 보낼 다중선택 체크박스(09-22) — 여러 부품 STL을 한 판에 얹어 같이 슬라이싱.
|
||||
const checked=stlSelection.has(file.relPath);
|
||||
const selBox=isStl?`<input type="checkbox" class="rb-file-stl-sel" title="슬라이서로 보내기 선택" ${checked?'checked':''} onclick="event.stopPropagation()" onchange="toggleStlSelect('${escAttr(file.relPath)}',this.checked)">`:'';
|
||||
return `<div class="rb-file-card${checked?' stl-selected':''}" data-rel="${escAttr(file.relPath)}">
|
||||
${selBox}
|
||||
<button class="rb-file-del" onclick="deleteFile('${escAttr(file.relPath)}')" title="삭제">✕</button>
|
||||
<a ${openAttrs}>
|
||||
<div class="rb-file-thumb">${thumb}</div>
|
||||
@@ -859,18 +877,47 @@ function renderFileCard(file){
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// K2 프린터 앱의 "🔧 STL 편집기"(Three.js, cad-editor-app.html)를 그대로 재사용 — STL 목록은
|
||||
// 워크스페이스 전체를 훑으므로(findStlFiles) 작업실 파일도 이미 그 목록에 잡힌다. ?path= 로
|
||||
// 작업실 프로젝트 relPath → 워크스페이스 전체 기준 상대경로. K2 CAD/슬라이서 라우트
|
||||
// (/api/cad/file, /api/cad/list 등)는 findStlFiles()로 워크스페이스 전체를 훑으므로
|
||||
// 이 접두어(workshop/<projectId>/)만 붙이면 작업실 파일도 그대로 잡힌다.
|
||||
function workshopFilePath(relPath){
|
||||
const proj=currentProject();
|
||||
return proj?('workshop/'+proj.id+'/'+relPath):null;
|
||||
}
|
||||
|
||||
// K2 프린터 앱의 "🔧 STL 편집기"(Three.js, cad-editor-app.html)를 그대로 재사용 — ?path= 로
|
||||
// 어느 파일을 열지 지정하면 편집기가 자동으로 로드한다(cad-editor-view.js 09-22 추가).
|
||||
function openCadEditorForFile(relPath){
|
||||
const proj=currentProject();
|
||||
if(!proj)return;
|
||||
const workspacePath='workshop/'+proj.id+'/'+relPath;
|
||||
const workspacePath=workshopFilePath(relPath);
|
||||
if(!workspacePath)return;
|
||||
const w=1320,h=860;
|
||||
const left=Math.max(0,(screen.width-w)/2), top=Math.max(0,(screen.height-h)/2);
|
||||
window.open('/html/cad-editor-app.html?path='+encodeURIComponent(workspacePath),'cad-editor',`width=${w},height=${h},left=${left},top=${top},menubar=no,toolbar=no,location=no,status=no,resizable=yes`);
|
||||
}
|
||||
|
||||
// ── 슬라이서 연동(09-22) ────────────────────────────────────────────────────
|
||||
// STL 카드 체크박스로 여러 부품(예: 스캐너 rig의 base_plate+head_plate+turntable 등)을
|
||||
// 골라 K2 슬라이서 팝업(slicer-app.html)에 한 번에 올린다. 실제 로드는 slicer-view.js의
|
||||
// ?load=<워크스페이스 상대경로,...> 지원(09-22 추가)이 처리.
|
||||
function toggleStlSelect(relPath,checked){
|
||||
if(checked)stlSelection.add(relPath);else stlSelection.delete(relPath);
|
||||
const card=document.querySelector(`.rb-file-card[data-rel="${CSS.escape(relPath)}"]`);
|
||||
if(card)card.classList.toggle('stl-selected',checked);
|
||||
const btn=document.getElementById('stl-slice-btn');
|
||||
const countEl=document.getElementById('stl-sel-count');
|
||||
if(countEl)countEl.textContent=String(stlSelection.size);
|
||||
if(btn)btn.disabled=stlSelection.size===0;
|
||||
}
|
||||
|
||||
function openSlicerForSelected(){
|
||||
if(!stlSelection.size)return;
|
||||
const paths=[...stlSelection].map(workshopFilePath).filter(Boolean);
|
||||
if(!paths.length)return;
|
||||
const w=1320,h=860;
|
||||
const left=Math.max(0,(screen.width-w)/2), top=Math.max(0,(screen.height-h)/2);
|
||||
window.open('/html/slicer-app.html?load='+encodeURIComponent(paths.join(',')),'k2-slicer',`width=${w},height=${h},left=${left},top=${top},menubar=no,toolbar=no,location=no,status=no,resizable=yes`);
|
||||
}
|
||||
|
||||
function fmtFileSize(n){
|
||||
if(!n&&n!==0)return '';
|
||||
if(n<1024)return n+'B';
|
||||
|
||||
Reference in New Issue
Block a user