From d34dd10ba7ce6d98eae93f7abcf9cd551f6d9e14 Mon Sep 17 00:00:00 2001 From: kim Date: Fri, 18 Sep 2026 00:31:38 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=8F=85=EB=A6=BD=20=EC=B9=B4=EB=A9=94?= =?UTF-8?q?=EB=9D=BC=EC=97=90=20ONVIF=20PTZ=20=EC=A7=80=EC=9B=90=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(=ED=94=84=EB=A6=B0=ED=84=B0=EC=8B=A4=20?= =?UTF-8?q?=EC=B9=B4=EB=A9=94=EB=9D=BC=20=EB=93=B1=EB=A1=9D)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit extra-cameras API/UI에 ptz·onvifPort 필드 추가하고, 프린터실 머큐시스 MC230 카메라를 PTZ 지원 독립 카메라로 등록. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01PppKcQmeTJDQCpfcgKietu --- .smallclaw/config.json | 14 +++++++++++++- src/gateway/routes/nvr.ts | 4 +++- web-ui/html/nvr-app.html | 4 +++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/.smallclaw/config.json b/.smallclaw/config.json index e53901a..aa41b88 100644 --- a/.smallclaw/config.json +++ b/.smallclaw/config.json @@ -378,7 +378,19 @@ "channelStream": { "3": 1 }, - "extraCameras": [] + "extraCameras": [ + { + "id": 1000, + "label": "프린터실", + "host": "192.168.0.32", + "rtspPort": 554, + "username": "octoprint", + "password": "vault:nvr.extraCameras.1000.password", + "rtspPath": "/stream1", + "ptz": true, + "onvifPort": 2020 + } + ] }, "writer": { "providers": {} diff --git a/src/gateway/routes/nvr.ts b/src/gateway/routes/nvr.ts index 8eac47b..5dee0a1 100644 --- a/src/gateway/routes/nvr.ts +++ b/src/gateway/routes/nvr.ts @@ -322,7 +322,7 @@ export function registerNvrRoutes(app: Express): void { app.post('/api/nvr/extra-cameras', (req: Request, res: Response) => { const user = (req as any).user; if (!user) return res.status(401).json({ error: 'Unauthorized' }); - const { label, host, rtspPort, username, password, rtspPath } = req.body || {}; + const { label, host, rtspPort, username, password, rtspPath, ptz, onvifPort } = req.body || {}; if (!String(host || '').trim()) return res.status(400).json({ error: 'host is required' }); const cm = getConfig(); const cfg = cm.getConfig() as any; @@ -338,6 +338,8 @@ export function registerNvrRoutes(app: Express): void { username: String(username || '').trim(), password: String(password || ''), rtspPath: String(rtspPath || '/stream1').trim(), + ptz: ptz === true, + onvifPort: onvifPort ? Number(onvifPort) : 80, }; cm.updateConfig({ nvr: { ...nvr, extraCameras: [...list, entry] } } as any); res.json({ success: true, id: nextId }); diff --git a/web-ui/html/nvr-app.html b/web-ui/html/nvr-app.html index b1cc5e9..ef9bfad 100644 --- a/web-ui/html/nvr-app.html +++ b/web-ui/html/nvr-app.html @@ -516,9 +516,11 @@ async function addExtraCamera(){ const username=prompt('RTSP 사용자명', 'camera'); const password=prompt('RTSP 비밀번호', ''); const rtspPath=prompt('RTSP 경로(메인 스트림)', '/stream1'); + const ptz=confirm('이 카메라가 팬/틸트(PTZ)를 지원하나요?'); + const onvifPort=ptz?(prompt('ONVIF 포트', '2020')||'2020'):'80'; try{ const r=await fetch('/api/nvr/extra-cameras',{method:'POST',headers:authH({'Content-Type':'application/json'}), - body:JSON.stringify({label,host,rtspPort:Number(rtspPort)||554,username,password,rtspPath})}); + body:JSON.stringify({label,host,rtspPort:Number(rtspPort)||554,username,password,rtspPath,ptz,onvifPort:Number(onvifPort)||80})}); const d=await r.json(); if(!r.ok||!d.success) throw new Error(d.error||'추가 실패'); loadChannels();