From 9641f689b835e44201e527ee81171d3c2fdff963 Mon Sep 17 00:00:00 2001 From: kim Date: Sun, 20 Sep 2026 13:46:37 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=A1=9C=EB=B4=87=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=EC=A0=9D=ED=8A=B8=20=EA=B4=80=EB=A6=AC=20=EB=8C=80=EC=8B=9C?= =?UTF-8?q?=EB=B3=B4=EB=93=9C(robot-app)=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 메카넘휠 이동로봇 프로젝트(파이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 --- src/gateway/routes/routes-robot.ts | 125 ++++++++++++ src/gateway/server.ts | 2 + web-ui/html/index.html | 1 + web-ui/html/robot-app.html | 318 +++++++++++++++++++++++++++++ 4 files changed, 446 insertions(+) create mode 100644 src/gateway/routes/routes-robot.ts create mode 100644 web-ui/html/robot-app.html diff --git a/src/gateway/routes/routes-robot.ts b/src/gateway/routes/routes-robot.ts new file mode 100644 index 0000000..5d00058 --- /dev/null +++ b/src/gateway/routes/routes-robot.ts @@ -0,0 +1,125 @@ +/** + * routes-robot.ts + * "로봇 프로젝트" 관리 대시보드 — 부품 리스트/단계별 작업/메모를 담은 단일 문서를 + * 사용자별로 저장한다. doctor/lawyer 케이스 앱과 달리 여러 건이 아니라 프로젝트 + * 하나뿐이라 통째로 GET/PUT하는 단일 JSON 문서 방식(케이스 배열이 아님) — 원자적 + * 쓰기(tmp+rename)는 routes-doctor-cases.ts와 동일한 관용구. + */ +import express from 'express'; +import fs from 'fs'; +import path from 'path'; +import { getUserWorkspace } from '../../config/config'; + +interface RobotPart { + id: string; + name: string; + qty: number; + unitPrice: number; + status: '검토중' | '주문완료' | '보유'; + memo: string; +} + +interface RobotTask { + id: string; + text: string; + done: boolean; +} + +interface RobotPhase { + id: string; + name: string; + tasks: RobotTask[]; +} + +interface RobotProjectData { + parts: RobotPart[]; + phases: RobotPhase[]; + notes: string; +} + +function robotProjectPath(username: string): string { + return path.join(getUserWorkspace(username), '.smallclaw', 'robot-project.json'); +} + +// 2026-09-19 project_robot_dog.md 메모 기준 — 첫 로드 시 빈 화면 대신 실제 진행상황을 반영. +// 사용자가 처음 편집해서 PUT하기 전까진 파일로 저장되지 않는다(읽기 전용 기본값). +function defaultSeed(): RobotProjectData { + return { + parts: [ + { id: 'p1', name: '라즈베리파이5 8GB(중고)', qty: 1, unitPrice: 200000, status: '주문완료', memo: '' }, + { id: 'p2', name: 'RPLidar A1(중고)', qty: 1, unitPrice: 60000, status: '주문완료', memo: '' }, + { id: 'p3', name: '메카넘휠 4WD 이중데크 섀시 키트', qty: 1, unitPrice: 44000, status: '검토중', memo: 'TT모터4+메카넘휠4+섀시2단' }, + { id: 'p4', name: '4채널 모터드라이버(L298N 등)', qty: 1, unitPrice: 10000, status: '검토중', memo: '' }, + { id: 'p5', name: 'MPU-6050 자이로/가속도', qty: 1, unitPrice: 2500, status: '검토중', memo: '' }, + { id: 'p6', name: '18650 셀 6개(3S2P)', qty: 1, unitPrice: 0, status: '보유', memo: '기존 보유분 재사용' }, + { id: 'p7', name: '3S-40A BMS', qty: 1, unitPrice: 2310, status: '검토중', memo: '' }, + { id: 'p8', name: '18650 홀더(6구)', qty: 1, unitPrice: 4000, status: '검토중', memo: '' }, + { id: 'p9', name: '와이어링/커넥터', qty: 1, unitPrice: 5000, status: '검토중', memo: '' }, + { id: 'p10', name: '나사/스탠드오프', qty: 1, unitPrice: 10000, status: '검토중', memo: '' }, + { id: 'p11', name: '재활용 NEMA17 스테퍼×5+리드스크류+브라켓', qty: 1, unitPrice: 0, status: '보유', memo: '폐기된 Anet A8 Plus에서 회수, 팔 텔레스코픽 관절용' }, + { id: 'p12', name: 'OAK-D-Lite 스테레오 카메라', qty: 1, unitPrice: 175000, status: '검토중', memo: '2단계(팔 추가) 때 그립 타겟팅용' }, + { id: 'p13', name: '로봇팔 키트(SO-101 계열, 방식 미정)', qty: 1, unitPrice: 200000, status: '검토중', memo: '옵션 A/B/E 중 미결정 — project_robot_dog.md 참고' }, + ], + phases: [ + { + id: 'ph1', name: '1단계: 소형 프로토타입(SLAM 검증)', + tasks: [ + { id: 't1', text: '라즈베리파이5 8GB 주문', done: true }, + { id: 't2', text: 'RPLidar A1 중고 주문', done: true }, + { id: 't3', text: '메카넘휠 섀시+모터드라이버 구매', done: false }, + { id: 't4', text: 'ROS2 Nav2 SLAM 소프트웨어 검증', done: false }, + ], + }, + { + id: 'ph2', name: '2단계: 팔 추가', + tasks: [ + { id: 't5', text: '팔 방식 최종 결정(SO-101 스케일업 vs AR4 vs 하이브리드)', done: false }, + { id: 't6', text: 'OAK-D-Lite 카메라 구매', done: false }, + { id: 't7', text: '스테퍼+리드스크류 텔레스코픽 관절 설계/제작', done: false }, + ], + }, + { + id: 'ph3', name: 'V2: 1m급 대형화(장기)', + tasks: [ + { id: 't8', text: '필요토크 계산 및 고토크모터 스펙 확정', done: false }, + { id: 't9', text: '대형 메카넘휠/알루미늄 프로파일 프레임 설계', done: false }, + { id: 't10', text: '하드웨어 전면 교체(소프트웨어 스택은 재사용)', done: false }, + ], + }, + ], + notes: '', + }; +} + +export function registerRobotRoutes( + app: express.Application, + getSessionUser: (req: express.Request) => any, +): void { + app.get('/api/robot/project', (req, res) => { + const session = getSessionUser(req); + if (!session) return res.status(401).json({ error: 'Unauthorized' }); + const p = robotProjectPath(session.username); + if (!fs.existsSync(p)) return res.json(defaultSeed()); + try { + res.json(JSON.parse(fs.readFileSync(p, 'utf-8'))); + } catch { + res.json(defaultSeed()); + } + }); + + app.put('/api/robot/project', (req, res) => { + const session = getSessionUser(req); + if (!session) return res.status(401).json({ error: 'Unauthorized' }); + const { parts, phases, notes } = req.body || {}; + if (!Array.isArray(parts) || !Array.isArray(phases)) { + return res.status(400).json({ error: 'parts/phases array required' }); + } + const data: RobotProjectData = { parts, phases, notes: String(notes || '') }; + const p = robotProjectPath(session.username); + fs.mkdirSync(path.dirname(p), { recursive: true }); + const tmp = `${p}.tmp`; + fs.writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf-8'); + fs.renameSync(tmp, p); + res.json({ success: true }); + }); +} diff --git a/src/gateway/server.ts b/src/gateway/server.ts index e3df8b0..1c48bb8 100644 --- a/src/gateway/server.ts +++ b/src/gateway/server.ts @@ -95,6 +95,7 @@ import { registerDentalRoutes } from './routes/routes-dental'; import { registerDetectiveDateRoutes } from './routes/routes-detective-dates'; import { registerLawyerCaseRoutes } from './routes/routes-lawyer-cases'; import { registerDoctorCaseRoutes } from './routes/routes-doctor-cases'; +import { registerRobotRoutes } from './routes/routes-robot'; import { caseFilesDir, resolveUploadPath, registerWopiRoutes } from './routes/case-storage'; import { getConfig, @@ -4611,6 +4612,7 @@ registerLawyerCaseRoutes(app, getSessionUser, isPathInsideDir); registerWopiRoutes(app, '/api/lawyer'); registerDoctorCaseRoutes(app, getSessionUser, isPathInsideDir); registerWopiRoutes(app, '/api/doctor'); +registerRobotRoutes(app, getSessionUser); // Redirect legacy top-level HTML routes into /html/ after web-ui reorganization. diff --git a/web-ui/html/index.html b/web-ui/html/index.html index 9378509..ed5ced3 100644 --- a/web-ui/html/index.html +++ b/web-ui/html/index.html @@ -96,6 +96,7 @@ + diff --git a/web-ui/html/robot-app.html b/web-ui/html/robot-app.html new file mode 100644 index 0000000..33eb99e --- /dev/null +++ b/web-ui/html/robot-app.html @@ -0,0 +1,318 @@ + + + + + +🤖 로봇 프로젝트 + + + + + + + +
+ ← 홈 +

🤖 로봇 프로젝트

+ + + +
+ +
+
검토중
0원
+
주문완료
0원
+
보유
0원
+
총 예상비용
0원
+
+ +
+ + + +
+ +
+
+ + + + + + +
이름수량단가상태메모소계
+ +
+ +
+
+ +
+ +
+ +
+
+ + + +