규칙(workshop-layout.ts 한 곳에 정의, 도구·스크립트가 공유): workshop/<id>/CAD/(.scad+STL 같은 폴더) · CAD/참고/(비인쇄 참고 모델) · CAD/다운로드/ · 출력/ · 사진/ · 영상/ · 조립설명서/ print3d/K2웹슬라이서/ · 모델검색/ · 일회성/ · profiles/ 인쇄용 STL은 .scad 옆에 두되 참고용은 CAD/참고/로 분리 유지(09-22 "참고용을 인쇄 대상으로 착각" 방지) - 이동 스크립트 scripts/migrate-workshop-layout.ts: 기본 dry-run, --apply(이동만·덮어쓰기 없음·충돌은 건너뛰고 보고), --undo <manifest>. .scad 경로 참조(조립 명세)와 문서 문구(CAD/print→CAD 등) 갱신, 빈 폴더만 정리. 멱등 - 도구가 같은 규칙으로 저장: print3d_model(모델→CAD/다운로드, G-code→출력, 프로젝트 없으면 print3d/일회성), model_download(CAD/다운로드 또는 print3d/모델검색), K2 웹 슬라이서(print3d/K2웹슬라이서). 옛 print3d/<프로젝트이름>/ 제거 - 채팅 프롬프트: STL은 .scad와 같은 폴더(옛 09-22 CAD/print 하위폴더 규칙 폐지), 프로젝트 폴더 규칙 안내 - 파일 탭: 권장 폴더를 비어 있어도 업로드 대상으로 표시하고 규칙 순서로 정렬 - 테스트 34건(규칙 표, 멱등, 충돌 미덮어쓰기, apply→undo 왕복 무손실, saveDirs, print3d 계획) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
110 lines
5.7 KiB
TypeScript
110 lines
5.7 KiB
TypeScript
/**
|
|
* 작업실 프로젝트 파일 폴더 정돈 스크립트(2026-09-25).
|
|
* npx tsx scripts/migrate-workshop-layout.ts [--user <이름>] # 계획만 출력(dry-run, 기본)
|
|
* npx tsx scripts/migrate-workshop-layout.ts --apply [--user <이름>] # 실제 이동 + 프로젝트 데이터 경로 갱신
|
|
* npx tsx scripts/migrate-workshop-layout.ts --undo <manifest.json> # 마지막 이동 되돌리기
|
|
* 규칙은 src/gateway/routes/workshop-layout.ts. 이동만 하고 삭제/덮어쓰기는 하지 않는다(충돌은 건너뛰고 보고).
|
|
* --apply는 되돌리기용 manifest를 <workspace>/.smallclaw/ 에 남긴다.
|
|
*/
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import { getConfig } from '../src/config/config';
|
|
import { loadWorkshop, saveWorkshop } from '../src/gateway/routes/workshop-storage';
|
|
import { planProjectMoves, planPrint3dMoves, pruneEmptyDirs, projectDir, renameTextPaths, MovePlan, MoveConflict } from '../src/gateway/routes/workshop-layout';
|
|
|
|
const args = process.argv.slice(2);
|
|
const apply = args.includes('--apply');
|
|
const undoIdx = args.indexOf('--undo');
|
|
const userIdx = args.indexOf('--user');
|
|
|
|
function workspaceOf(user: string): string {
|
|
return path.join(getConfig().getConfigDir(), 'users', user, 'workspace');
|
|
}
|
|
|
|
if (undoIdx >= 0) {
|
|
const mf = JSON.parse(fs.readFileSync(args[undoIdx + 1], 'utf-8'));
|
|
let n = 0;
|
|
for (const m of [...mf.moves].reverse() as MovePlan[]) {
|
|
const from = path.join(projectDir(mf.workspace, m.project), m.to);
|
|
const to = path.join(projectDir(mf.workspace, m.project), m.from);
|
|
if (!fs.existsSync(from) || fs.existsSync(to)) { console.log(`건너뜀: ${m.project}/${m.to}`); continue; }
|
|
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
fs.renameSync(from, to); n++;
|
|
}
|
|
for (const m of [...(mf.print3d || [])].reverse() as { from: string; to: string }[]) {
|
|
const from = path.join(mf.workspace, m.to), to = path.join(mf.workspace, m.from);
|
|
if (!fs.existsSync(from) || fs.existsSync(to)) continue;
|
|
fs.mkdirSync(path.dirname(to), { recursive: true }); fs.renameSync(from, to); n++;
|
|
}
|
|
// 프로젝트 데이터(assembly.scad 경로)도 원복
|
|
const data = loadWorkshop(mf.workspace);
|
|
for (const p of data.projects) if (p.assembly && mf.scadRemap?.[p.id]) p.assembly.scad = mf.scadRemap[p.id].from;
|
|
saveWorkshop(mf.workspace, data);
|
|
console.log(`되돌림: 파일 ${n}개 (빈 폴더는 필요 시 다시 생성됨)`);
|
|
process.exit(0);
|
|
}
|
|
|
|
const users = userIdx >= 0 ? [args[userIdx + 1]] : fs.readdirSync(path.join(getConfig().getConfigDir(), 'users'));
|
|
for (const user of users) {
|
|
const ws = workspaceOf(user);
|
|
if (!fs.existsSync(path.join(ws, '.smallclaw', 'workshop.json'))) continue;
|
|
const data = loadWorkshop(ws);
|
|
const allMoves: MovePlan[] = [];
|
|
const allConflicts: MoveConflict[] = [];
|
|
for (const p of data.projects) {
|
|
if (!fs.existsSync(projectDir(ws, p.id))) continue;
|
|
const { moves, conflicts } = planProjectMoves(ws, p.id);
|
|
allMoves.push(...moves); allConflicts.push(...conflicts);
|
|
}
|
|
console.log(`\n=== 사용자 ${user}: 이동 ${allMoves.length}개, 충돌 ${allConflicts.length}개 ${apply ? '(적용)' : '(계획만 — dry-run)'}`);
|
|
let cur = '';
|
|
for (const m of allMoves) {
|
|
if (m.project !== cur) { cur = m.project; console.log(`\n[${data.projects.find(p => p.id === cur)?.name ?? cur}] (${cur})`); }
|
|
console.log(` ${m.from} → ${m.to}`);
|
|
}
|
|
for (const c of allConflicts) console.log(` ⚠ 충돌 건너뜀: ${c.project}/${c.from} → ${c.to} (${c.reason})`);
|
|
const p3 = planPrint3dMoves(ws);
|
|
if (p3.length) {
|
|
console.log(`\n[워크스페이스 print3d/] 이동 ${p3.length}개`);
|
|
for (const m of p3) console.log(` ${m.from} → ${m.to}`);
|
|
}
|
|
if (!apply || (!allMoves.length && !p3.length)) continue;
|
|
|
|
// 1) 파일 이동(같은 파일시스템 rename — 용량과 무관하게 즉시, 내용 그대로)
|
|
const done: MovePlan[] = [];
|
|
for (const m of allMoves) {
|
|
const root = projectDir(ws, m.project);
|
|
const to = path.join(root, m.to);
|
|
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
fs.renameSync(path.join(root, m.from), to);
|
|
done.push(m);
|
|
}
|
|
// 2) .scad 경로 참조(조립 명세) + 문서 문구 갱신
|
|
const scadRemap: Record<string, { from: string; to: string }> = {};
|
|
for (const p of data.projects) {
|
|
if (p.assembly?.scad) {
|
|
const prefix = `workshop/${p.id}/`;
|
|
const mv = done.find(m => m.project === p.id && prefix + m.from === p.assembly!.scad);
|
|
if (mv) { scadRemap[p.id] = { from: p.assembly.scad, to: prefix + mv.to }; p.assembly.scad = prefix + mv.to; }
|
|
}
|
|
if (p.description) p.description = renameTextPaths(p.description);
|
|
if (p.notes) p.notes = renameTextPaths(p.notes);
|
|
}
|
|
saveWorkshop(ws, data); // 저장 때 이전 상태가 이력에 자동 보존된다
|
|
// 3) 비게 된 폴더 정리(빈 폴더만)
|
|
for (const p of data.projects) {
|
|
const removed = fs.existsSync(projectDir(ws, p.id)) ? pruneEmptyDirs(projectDir(ws, p.id)) : [];
|
|
if (removed.length) console.log(` 빈 폴더 정리 [${p.id}]: ${removed.join(', ')}`);
|
|
}
|
|
for (const m of p3) {
|
|
const to = path.join(ws, m.to);
|
|
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
fs.renameSync(path.join(ws, m.from), to);
|
|
}
|
|
const oldMS = path.join(ws, 'print3d', 'model-search');
|
|
if (fs.existsSync(oldMS)) pruneEmptyDirs(oldMS), (fs.readdirSync(oldMS).length === 0 && fs.rmdirSync(oldMS));
|
|
const manifest = path.join(ws, '.smallclaw', `workshop-layout-migration-${Date.now()}.json`);
|
|
fs.writeFileSync(manifest, JSON.stringify({ workspace: ws, at: new Date().toISOString(), moves: done, print3d: p3, scadRemap }, null, 2));
|
|
console.log(`\n완료 — 되돌리기: npx tsx scripts/migrate-workshop-layout.ts --undo ${manifest}`);
|
|
}
|