저장 개편(리뷰 #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>
79 lines
3.7 KiB
TypeScript
79 lines
3.7 KiB
TypeScript
// workshop-storage.ts 프로젝트 개요 문서(projectToReadme) 단위 테스트 (2026-09-23)
|
||
// 대시보드 데이터 → 마크다운 'get' 출력. readme 파일 생성은 철회됨(개요 탭으로 이동).
|
||
import test from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { projectToReadme, WorkshopProject } from '../src/gateway/routes/workshop-storage.js';
|
||
|
||
function sampleProject(over: Partial<WorkshopProject> = {}): WorkshopProject {
|
||
return {
|
||
id: 'proj_test',
|
||
name: 'SO-101 로봇팔',
|
||
parts: [
|
||
{ id: 'p1', name: 'STS3215 서보', qty: 12, unitPrice: 20000, status: '검토중', memo: 'Leader 6 + Follower 6', links: [{ id: 'lk1', label: '구매처', url: 'https://example.com' }] },
|
||
{ id: 'p2', name: '나사 세트', qty: 1, unitPrice: 0, status: '보유', memo: '', links: [] },
|
||
],
|
||
phases: [
|
||
{ id: 'ph1', name: '준비', tasks: [{ id: 't1', text: 'STL 다운로드', done: true }, { id: 't2', text: '출력', done: false }] },
|
||
],
|
||
notes: 'K2로 출력한다',
|
||
...over,
|
||
};
|
||
}
|
||
|
||
test('projectToReadme: 개요 섹션은 description이 있을 때만', () => {
|
||
const withDesc = projectToReadme(sampleProject({ description: 'LeRobot SO-101 오픈소스 로봇팔 조립 프로젝트' }));
|
||
assert.ok(withDesc.includes('# SO-101 로봇팔'));
|
||
assert.ok(withDesc.includes('## 개요\nLeRobot SO-101 오픈소스 로봇팔 조립 프로젝트'));
|
||
|
||
const withoutDesc = projectToReadme(sampleProject());
|
||
assert.ok(!withoutDesc.includes('## 개요'));
|
||
// 부품/작업/메모는 항상 실린다
|
||
assert.ok(withoutDesc.includes('- [검토중] STS3215 서보 × 12 = 240,000원'));
|
||
assert.ok(withoutDesc.includes('- [x] STL 다운로드'));
|
||
assert.ok(withoutDesc.includes('## 메모'));
|
||
});
|
||
|
||
test('projectToReadme: 부품 링크를 들여쓰기로 실음', () => {
|
||
const md = projectToReadme(sampleProject());
|
||
assert.ok(md.includes(' - 구매처: https://example.com'));
|
||
});
|
||
// 프로젝트 단위 저장 API(2026-09-24)의 upsert/remove — 서버는 디스크의 현재 문서에
|
||
// 해당 프로젝트만 반영하므로 다른 프로젝트는 절대 덮이지 않아야 한다.
|
||
import { upsertProject, removeProject, WorkshopData } from '../src/gateway/routes/workshop-storage.js';
|
||
|
||
function sampleData(): WorkshopData {
|
||
const a = sampleProject({ id: 'proj_a', name: 'A' });
|
||
const b = sampleProject({ id: 'proj_b', name: 'B' });
|
||
return { projects: [a, b], activeProjectId: 'proj_a' };
|
||
}
|
||
|
||
test('upsertProject: 기존 프로젝트는 교체, 나머지는 보존', () => {
|
||
const data = sampleData();
|
||
const edited = sampleProject({ id: 'proj_b', name: 'B-수정됨', notes: '채팅 도구가 저장' });
|
||
upsertProject(data, edited);
|
||
assert.equal(data.projects.length, 2);
|
||
assert.equal(data.projects[1].name, 'B-수정됨');
|
||
// 다른 프로젝트(A)의 내용은 그대로 — 전체문서 PUT이던 시절엔 여기가 유실됐다
|
||
assert.equal(data.projects[0].name, 'A');
|
||
assert.equal(data.projects[0].parts[0].name, 'STS3215 서보');
|
||
});
|
||
|
||
test('upsertProject: 없는 프로젝트는 추가, 활성 포인터가 무효하면 복구', () => {
|
||
const data = sampleData();
|
||
data.activeProjectId = 'proj_gone';
|
||
upsertProject(data, sampleProject({ id: 'proj_new', name: '새것' }));
|
||
assert.equal(data.projects.length, 3);
|
||
assert.equal(data.activeProjectId, 'proj_a');
|
||
});
|
||
|
||
test('removeProject: 삭제되면 true, 활성 포인터가 가리키던 프로젝트면 첫 프로젝트로', () => {
|
||
const data = sampleData();
|
||
assert.equal(removeProject(data, 'proj_a'), true);
|
||
assert.equal(data.projects.length, 1);
|
||
assert.equal(data.activeProjectId, 'proj_b');
|
||
});
|
||
|
||
test('removeProject: 없는 프로젝트면 false(404 응답용)', () => {
|
||
assert.equal(removeProject(sampleData(), 'proj_none'), false);
|
||
});
|